Импортируем категории товаров
Сегодня мы доработаем модель категорий товаров, импортируем старые из miniShop2 и обновим админку.
Новые категории хранят свои текстовые колонки в отдельной таблице, так что нам нужно будет создать модель и для неё, а потом написать всякие вспомогательные методы в админке для удобного использования переводов.
Помимо этого, категории еще могут иметь картинку, но тут мы просто используем компонент upload-file, который написали вчера для юзеров.
Модель Language
Первым делом создаём модель для работы с языками - это нужно для создания связи с другими моделями.
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Database\Eloquent\Relations\HasManyThrough;
/**php
* @property string $lang
* @property string $title
* @property bool $active
* @property int $rank
*
* @property-read CategoryTranslation[] $categoriesTranslations
* @property-read Category[] $categories
*/
class Language extends Model
{
public $timestamps = false;
protected $primaryKey = 'lang';
protected $keyType = 'string';
protected $fillable = ['code', 'title', 'active', 'rank'];
protected $casts = [
'active' => 'boolean',
];
public function categoriesTranslations(): HasMany
{
return $this->hasMany(CategoryTranslation::class, 'lang');
}
public function categories(): HasManyThrough
{
return $this->hasManyThrough(
Category::class,
CategoryTranslation::class,
'lang',
'id',
'lang',
'category_id'
);
}
}
Обратите внимание на связь с категориями через модель CategoryTranslation. Через неё Eloquent сделает присоединение таблицы CategoryTranslation и получит все категории на нужном языке. Не знаю, где это может пригодиться, но пусть будет.
Модель CategoryTranslation
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
/**
* @property int $category_id
* @property string $lang
* @property string $title
* @property string $description
*
* @property-read Category $category
* @property-read Language $language
*/
class CategoryTranslation extends Model
{
use Traits\CompositeKey;
public $timestamps = false;
protected $primaryKey = ['category_id', 'lang'];
protected $fillable = ['category_id', 'lang', 'title', 'description'];
public function category(): BelongsTo
{
return $this->belongsTo(Category::class);
}
public function language(): BelongsTo
{
return $this->belongsTo(Language::class, 'lang');
}
}
Эта модель использует ещё один новый трейт CompositeKey, потому что, что Eloquent не поддерживает составные ключи по умолчанию - и нам надо переопределить некоторые методы модели для правильной работы.
Сам код трейта я приводить не буду, посмотрите в исходниках, там ничего интересного. Когда-то давно я столкнулся с этой проблемой и нашёл решение, теперь таскаю этот трейт из проекта в проект, по необходимости.
Еще можно обратить внимание на связь language(), где явно указано по какой колонке привязывать записи. Для метода category() это не требуется, потому что Eloquent автоматически будет смотреть на колонку category_id, исходя из имени связи. А вот для языка у нас не lang_id, а просто lang - поэтому её и нужно указать.
Модель Category
Эта модель у нас уже есть, нужно добавить новые свойства:
/**
* @property int $id
* @property int $parent_id
* @property ?int $file_id
* @property string $alias
* @property string $uri
* @property bool $active
* @property int $rank
* @property int $remote_id
* @property Carbon $created_at
* @property Carbon $updated_at
*
* @property-read Category $parent
* @property-read CategoryTranslation[] $translations
* @property-read File $file
* @property-read Category[] $children
* @property-read Product[] $products
*/
Из-за того, что теперь категории могут быть вложены друг в друга, у нас появилась колонка uri, которая будет хранить полный адрес категории. Мне не хочется следить за её обновлением самостоятельно, так что пропишу функцию на событие сохранения модели:
protected static function booted(): void
{
static::saving(static function (self $model) {
$uri = [$model->alias];
if ($model->parent) {
array_unshift($uri, $model->parent->uri);
}
$model->uri = implode('/', $uri);
});
}
Здесь мы генерируем uri модели, складывая её alias с родительским uri.
Осталось прописать связи с другими моделями:
public function parent(): BelongsTo
{
return $this->belongsTo(__CLASS__);
}
public function translations(): HasMany
{
return $this->hasMany(CategoryTranslation::class);
}
public function file(): BelongsTo
{
return $this->belongsTo(File::class);
}
public function children(): HasMany
{
return $this->hasMany(__CLASS__, 'parent_id');
}
public function products(): HasMany
{
return $this->hasMany(Product::class);
}
Связи parent и children ссылаются сами на себя, то есть на саму таблицу категорий, так что мы используем встроенную в PHP константу __CLASS__.
Засеиваем языки
Создаём новый сид в core/db/seeds/Languages.php, так как мы планируем на них везде ссылаться.
<?php
use App\Models\Language;
use Phinx\Seed\AbstractSeed;
class Languages extends AbstractSeed
{
protected array $data = [
'ru' => [
'title' => 'Русский',
'rank' => 0,
],
'en' => [
'title' => 'English',
'rank' => 1,
],
];
public function run(): void
{
foreach ($this->data as $id => $data) {
/** @var Language $record */
if (!$record = Language::query()->find($id)) {
$record = new Language();
$record->lang = $id;
}
$record->fill($data);
$record->save();
}
}
}
Можно сразу же его и запустить: composer db:seed-one Languages
Импортируем категории из miniShop2
Общая логика здесь та же, что и при импорте пользователей, с одним отличием: нам нужно присоединить еще и переводы. К счастью, в исходном проекте на MODX использовалось дополнение Lingua, которое хранит тексты в отдельной таблице. Вот её и будем подключать.
Там тексты на немецком и француском, но мы сделаем вид, что это русский и английский!
Во время запуска сида я столкнулся с проблемой проверки уникальности индекса таблицы category_translations - она ругалась, что нельзя создать 2 записи на 1 языке с одинаковым title. Причём, если title пустой - это всё равно считается за значение. Так как не у всех категорий есть перевод на обоих языках, мне вылетало вот такое
Пришлось откатить миграции до юзеров, сделать $table->string('title')->nullable(); в миграции категорий и накатить снова. Null не считается значением, и не влияет на уникальность индекса.
Теперь можно и написать сам сидер в core/db/seeds/Categories.php.
<?php
use App\Models\Category;
use App\Models\File;
use Phinx\Seed\AbstractSeed;
use Slim\Psr7\UploadedFile;
use Vesp\Services\Eloquent;
class Categories extends AbstractSeed
{
public function getDependencies(): array
{
return ['Languages'];
}
public function run(): void
{
$tvImageId = 1;
// Выбираем категории вместе с переводами и картинками
$pdo = (new Eloquent())->getDatabaseManager()->getPdo();
$stmt = $pdo->query(
"SELECT `r`.*, `tv`.`value` AS `image`, `ln`.`pagetitle` as `pagetitle_fr`, `ln`.`content` as `content_fr`
FROM `modx_site_content` `r`
LEFT JOIN `modx_site_tmplvar_contentvalues` `tv` ON `tmplvarid` = '$tvImageId' AND `contentid` = `r`.`id`
LEFT JOIN `modx_lingua_site_content` `ln` ON `resource_id` = `r`.`id`
WHERE `class_key` = 'msCategory' ORDER BY `parent`,`r`.`id`"
);
$parents = [];
while ($row = $stmt->fetch(PDO::FETCH_ASSOC)) {
// Проверяем наличие категории по remote_id
if (!$category = Category::query()->where('remote_id', $row['id'])->first()) {
$category = new Category();
$category->remote_id = $row['id'];
}
// Замолняем основные поля
$category->alias = trim($row['alias']);
$category->uri = preg_replace('#^shop/#', '', trim($row['uri'], '/'));
$category->active = empty($row['deleted']) && !empty($row['published']);
$category->rank = $row['menuindex'];
$category->created_at = $row['createdon'];
$category->updated_at = $row['editedon'] ?: null;
// Заменяем id родителя, если есть
$category->parent_id = $parents[$row['parent']] ?? null;
// Если у категории есть картинка - загружаем плейсхолдер
if (!empty($row['image'])) {
$filename = tempnam(getenv('UPLOAD_DIR'), 'img_');
file_put_contents($filename, file_get_contents('https://i.pravatar.cc/500'));
$data = new UploadedFile($filename, basename($filename), mime_content_type($filename));
if (!$file = $category->file) {
$file = new File();
}
$file->uploadFile($data);
$category->update(['file_id' => $file->id]);
unlink($filename);
}
// --
// Сохраняем категорию
$category->save();
// И сразу создаём переводы через связь модели
$category->translations()->updateOrCreate(['lang' => 'ru'], [
'title' => trim($row['pagetitle']) ?: null,
'description' => trim($row['content']) ?: null,
]);
$category->translations()->updateOrCreate(['lang' => 'en'], [
'title' => trim($row['pagetitle_fr']) ?: null,
'description' => trim($row['content_fr']) ?: null,
]);
// Обязательно сохраняем соответствие старого и нового id для
// добавления дочерних категорий
$parents[$category->remote_id] = $category->id;
}
}
}
Небольшая хитрость здесь в том, чтобы выбирать исходные категории по возрастанию родителя (колонка parent), чтобы мы сначала создали корневые категории, а потом добавили в них дочерние.
id из MODX и id в Vesp сопоставляются через массив $parents, в который мы закидываем новые записи по мере создания категорий.
В проекте на MODX картинка категории хранится в TV, поэтому я подключаю таблицу modx_site_tmplvar_contentvalues, но настоящих файлов у нас нет, так что я создаю случайные плейсхолдеры. Оставил просто для примера импорта из TV.
После запуска скрипта вижу новые записи в БД:
Плагин для вывода текста в админке
Здесь и далее мы будем выводить тексты от присоединённых моделей перевода, поэтому лучше сразу предусмотреть для этого специальную функцию.
Я предпочитаю создавать плагин с утилитами в admin/plugins/utils.js для всякого такого:
// Ищет требуемый ключ в переданном объете и возвращает строку,
// если она не пустая
function translate(translations, field, lang = 'ru') {
const data = translations.filter((i) => i.lang === lang)
if (data.length > 0 && data[0][field] && data[0][field].length > 0) {
return data[0][field]
}
return null
}
// Плагин Nuxt должен возвращать вот такой объект
export default ({app}, inject) => {
// Регистрируем глобальную функцию приложения
inject('translate', (translations, field = 'title') => {
// Пробуем вывести строку для требуемого языка
let text = translate(translations, field, app.i18n.locale)
if (!text && app.i18n.locale !== app.i18n.defaultLocale) {
// Или строку языка по умолчанию
text = translate(translations, field, app.i18n.defaultLocale)
}
// Если переводов нет - надо уведомить разработчика, чтобы добавил
if (!text) {
console.info(`Could not find translation for ${field} in ${JSON.stringify(translations)}`)
}
return text
})
}
Побробнее про регистрацию плагинов Nuxt 2 можно почитать в документации.
А мы просто добавляем его в nuxt.config.js:
Config.plugins = ['@/plugins/utils.js']
После этого можно везде использовать функцию $translate().
Дорабатываем таблицу категорий
Сначала нужно подготовить бэкенд. Я создаю новый трейт для контроллеров Controllers/Traits/TranslateModelController.php:
<?php
namespace App\Controllers\Traits;
use Illuminate\Database\Eloquent\Model;
trait TranslateModelController
{
protected function afterSave(Model $record): Model
{
if ($translations = $this->getProperty('translations')) {
foreach ($translations as $data) {
$record->translations()->updateOrCreate(['lang' => $data['lang']], $data);
}
}
return $record;
}
}
Трейт очень простой - обрабатывает присланные тексты после сохранения модели.
Дальше крутим Controllers/Admin/Categories.php.
class Categories extends ModelController
{
// Наши новые трейты для переводов и загрузки файла
use FileModelController;
use TranslateModelController;
//...
protected $attachments = ['file'];
// Подключаем нужные модели при выводе для редактирования
protected function beforeGet(Builder $c): Builder
{
$c->with('file:id,updated_at');
$c->with('translations');
return $c;
}
protected function beforeCount(Builder $c): Builder
{
// Поиск категории теперь проводится по таблице текстов
if ($query = trim($this->getProperty('query', ''))) {
$c->whereHas('translations', static function (Builder $c) use ($query) {
$c->where('title', 'LIKE', "%$query%");
$c->orWhere('description', 'LIKE', "%$query%");
});
}
// Возможность прятать некоторые категории,
// пригодится для автодополнения
if ($exclude = $this->getProperty('exclude')) {
if (is_array($exclude)) {
$c->whereNotIn('id', $exclude);
} else {
$c->where('id', '!=', $exclude);
}
}
return $c;
}
// Нужные связи для вывода таблицы категорий
protected function afterCount(Builder $c): Builder
{
$c->with('translations:category_id,lang,title');
$c->with('parent:id', 'parent.translations:category_id,lang,title');
$c->with('file:id,updated_at');
$c->withCount('children');
$c->withCount('products');
return $c;
}
protected function beforeSave(Model $record): ?ResponseInterface
{
/** @var Category $record */
if (!$record->parent_id) {
// Фронт может прислать 0 или пустую строку в качестве id родителя
// Это вызовет ошибку внешнего ключа, так что форсируем null
$record->parent_id = null;
}
// Обрабатываем файлы методом из трейта
if ($error = $this->processFiles($record)) {
return $error;
}
// Уникальность alias теперь проверяем с учётом родителя
$c = Category::query()->where('alias', $record->alias);
if ($record->exists) {
$c->where('id', '!=', $record->id);
}
if ($record->parent_id) {
$c->where('parent_id', $record->parent_id);
}
if ($c->count()) {
return $this->failure('errors.category.alias_exists');
}
return null;
}
Далее нужно обновить и таблицу категорий admin/pages/categories.vue. В самой таблице у нас добавляется специальное оформление ячеек:
<vesp-table ....>
<template #cell(title)="{item}">
{{ $translate(item.translations) }}
<div class="small text-muted">{{ item.uri }}</div>
</template>
<template #cell(parent)="{item}">
<template v-if="item.parent">
{{ $translate(item.parent.translations) }}
</template>
</template>
<template #cell(file)="{value}">
<b-img
v-if="value.id"
:key="value.id"
:src="$image(value, {w: 150, h: 75, fit: 'crop'})"
:srcset="$image(value, {w: 300, h: 150, fit: 'crop'}) + ' 2x'"
width="150"
height="75"
alt=""
class="rounded"
/>
</template>
</vesp-table>
А в js коде добавляем только новые колонки:
<script>
export default {
//...
computed: {
// ...
fields() {
return [
{key: 'id', label: this.$t('components.table.columns.id'), sortable: true},
{key: 'file', label: this.$t('models.category.file')},
{key: 'title', label: this.$t('models.category.title'), sortable: true},
{key: 'parent', label: this.$t('models.category.parent')},
{key: 'products_count', label: this.$t('models.product.title_many')},
]
},
}
}
</script>
В итоге у нас получается вот такая магия - имена категорий меняются при смене локализации админки:
Языком по умолчанию сейчас является английский, через это на видео некоторые строки выходят пыстыми - ведь что на английском они не заполнены. Меняем порядок языков и умолчание в nuxt.config.js
Config.i18n.defaultLocale = 'ru'
Config.i18n.locales = [
{code: 'ru', title: 'Русский'},
{code: 'en', title: 'English'},
]
Форма редактирования категории
Вот здесь может быть немного сложно для понимания.
Дело в том, что для удобной работы с переводами, мы добавим переключение языков прямо в форму. Но нужно сделать это универсально, ведь у нас еще будут как минимум товары с такой же функцией. Да и в другие проекты потом можно утащить, что тоже неплохо.
Поэтому нам понадобятся:
- Новый контроллер Controllers/Admin/Languages.php для получения списка языков системы
- Новый маршрут для него в core/routes.php
- Результат загрузки мы будем хранить в store приложения. Для этого добавим новый ключ в state, новый метод в mutations и заведём action для запроса в API.
- Всякие нужные методы для работы формы сгруппируем в один общий mixin для добавления формам. Это как трейт в PHP, только для Vue.
- Активный язык будем отображать для каждого поля ввода с помощью стилей CSS.
- Ну а сам язык формы будем переключать через отдельный компонент lang-tabs.vue
Контроллер с маршрутом описывать не буду, там ничего нового.
А вот хранилище (admin/store/index.js) мы меняем вот так:
export const state = () => ({
// ...
languages: [],
})
export const mutations = {
// ...
languages(state, payload) {
state.languages = payload
},
}
export const actions = {
async languages({commit, state}) {
if (!state.languages.length) {
try {
const {data} = await this.$axios.get('admin/languages', {params: {combo: true}})
commit('languages', data.rows)
} catch (e) {}
}
return state.languages
},
}
Вновь созданный action мы будем вызывать как this.$store.dispatch('languages') и в ответ получим языки системы. Загружать он их будет только один раз, а потом сохранит в this.$store.state.languages и будет выдавать уже оттуда. Можно сказать, это такое кэширование.
Именно на этой логике и основана наша примесь в admin/mixins/translations.js:
export default {
// Работаем до рендера компонента на странице
async beforeMount() {
try {
const translations = []
// Поулучаем языки
const languages = await this.$store.dispatch('languages')
// Проверяем значения для языков в моделе и создаем пустые переводы, если надо
languages.forEach((v) => {
const idx = this.record.translations.findIndex((i) => i.lang === v.lang)
translations.push(idx > -1 ? {...this.record.translations[idx]} : {...this.translationFields, lang: v.lang})
})
// Выставляем переводы модели в форму, получая ключи из translationFields
this.$set(this.record, 'translations', translations)
} catch (e) {
console.error(e)
}
},
data() {
return {
// Здесь каждый компонент должен указать, какие поля у него переводимые
// например {title: '', description: ''}
translationFields: {},
}
},
directives: {
// См. ниже
langIcon: {
update(el, {value}) {
if (!el.classList.contains('form-group')) {
el = el.querySelector('.form-group')
}
if (!el) {
return
}
el.classList.forEach((cls) => {
if (cls.startsWith('icon-')) {
el.classList.remove(cls)
}
})
el.classList.add('icon-' + value)
},
},
},
}
Сам по себе этот mixin, как и трейт в PHP, работать не будет - нам нужно импортировать его в форму категории.
import Translations from '@/mixins/translations.js'
export default {
// ...
mixins: [Translations],
data() {
return {
lang: this.$i18n.locale || 'ru',
translationFields: {title: '', description: ''},
}
},
// ...
}
При вызозе формы он:
- Добавит метод beforeMount(), который пробежится по указанным translationFields, проверит их ключи в переводах редактируемой модели и добавит путые, если нужно. Это чтобы избежать ошибок при создании новой категории, у которой еще не может быть переводов, и добавлении новых языков.
- Добавит директиву langIcon, котрая будет прикручивать специальный CSS класс для поля ввода, чтобы там появлялся флажочек.
Директива указывается непосредственно элементам формы, и передаёт их, элементы, в метод update(), где мы на чистом javascript можем сделать с ним всё, что угодно. Например, добавлять класс icon-lang-ru для отрисовки российского флага.
По наличию этих флажочков пользователь админки поймёт, что это специальные переводимые поля формы.
Теперь можно изменить шаблон формы категории:
<template>
<!-- -->
<b-form-group v-lang-icon="lang" :label="$t('models.category.title')">
<b-form-input
v-for="(translation, idx) in record.translations"
v-show="translation.lang === lang"
:key="idx"
v-model.trim="translation.title"
autofocus
required
/>
</b-form-group>
<!-- -->
</template>
Итак, v-lang-icon="lang" - это и есть наша директива, которая срабатывает на элемент формы и получает текущий активный язык. На этот момент уже отработал метод beforeMount() и в нашей моделе обязательно есть значения для каждого языка внутри record.translations - этот массив мы и перебираем для вывода стольких полей ввода.
Но видеть мы будем только то поле, которое соответствует активному языку, потому что v-show="translation.lang === lang". Все эти поля будут обязательны для ввода, а значение будет сохраняться в соответствующий ключ record.translations.
На сервер у нас должен улетать примерно такой массив:
{
"id": 20,
// ...
"file": null,
"translations": [
{
"category_id": 20,
"lang": "ru",
"title": "Дочерняя Категория",
"description": "Описание дочерней категории"
},
{
"category_id": 20,
"lang": "en",
"title": "Children Category",
"description": "Children Category Description"
}
]
}
Там его поймает и обработает наш трейт TranslateModelController.
Последний штрих - компонент для переключения активного lang, пусть он будет в admin/components/lang-tabs.vue:
<template>
<b-tabs v-model="tab" :pills="pills" :small="small" :align="align" :active-nav-item-class="activeClass">
<b-tab v-for="(language, idx) in languages" :key="idx">
<template #title>
<div class="d-flex align-items-center">
<b-img :src="require('~/assets/images/icon-lang-' + language.lang + '.svg')" width="20" height="20" />
<div class="ml-2">{{ language.title }}</div>
</div>
</template>
</b-tab>
</b-tabs>
</template>
<script>
export default {
name: 'LangTabs',
// ...
data() {
// Активная вкладка при загрузке зависит от текущего языка админки
let tab = 0
const languages = this.$store.state.languages
if (this.value && languages.length) {
tab = languages.findIndex((i) => i.lang === this.value)
}
return {tab}
},
computed: {
languages() {
return this.$store.state.languages
},
},
watch: {
// Меняем v-model при переключении вкладки
tab(newValue) {
if (this.languages[newValue]) {
this.myValue = this.languages[newValue].lang
}
},
},
}
</script>
Этот компонент тоже нужно импортировать в форму и вызвать в любом удобном месте. У меня он на самом верху:
<template>
<div>
<lang-tabs v-model="lang" />
<hr />
<b-row class="mt-4">
<!-- ... -->
Итоговый результат:
Заключение
Прекрасно понимаю, что вся эта магия Vue с хранилищами, примесями и директивами сбивает с толку, но для этого у нас и предусмотрены комментарии, чтобы вы задавали вопросы.
Весь смысл в том, чтобы вынести общие и повторяющиеся функции отдельно, а потом использовать их там, где нужно. Как видите, загрузку файла я уже не рассматриваю, она работает ровно так же, как и загрузка аватарки у юзеров.
На следующим уроке по работе с товарами я не буду рассказывать про работу с переводами - потому что мы просто используем общие компоненты, написанные сегодня.
Итоговый коммит со всеми изменениями.
0
👍
👎
❤️
🔥
😮
😢
😀
😡
276
16.08.2023, 11:11:44
11 комментариев
Дмитрий П.
16.08.2023, 15:45:46
А если у класса CategoryTranslations использовать не трейт CompositeKey, а наследовать класс Pivot? Он хорошо работает с композитным ключом из двух колонок, и как раз создан для связи many-to-many. Но, как показала практика, можно связь не настраивать и использовать для one-to-many. Или есть какие-то подводные камни? :)
Василий Наумкин
16.08.2023, 16:28:50
Так здесь дело не про отношения, а про составной первичный ключ.
У обычный моделей есть id, по которому модель можно выбрать и обновить/удалить. У CategoryTranslation есть category_id и lang, и только их сочетание уникально. То есть, у этой модели всегда нужно указывать 2 колонки для работы, вместо 1й.
Именно для того, чтобы такая модель нормально находилась, сохранялась и удалялась и нужен наш trait.
Если я чего-то не понимаю - приведи пример, пожалйста, как это можно сделать, наследуя класс Pivot. Как минимум там в модели должен указываться $primaryKey в виде массива колонок, в документации я такого не вижу.
Дмитрий П.
16.08.2023, 17:08:24
Эта модель как раз и использует составной первичный ключ, но нормальной документации я не нашел, только упоминание, что такой класс есть. Но если посмотреть исходники этой модели, то получается как-то так:
Миграция с композитным ключом:
Модель, которой нужно указать таблицу (по умолчанию Eloquent ее имя не очень красиво определяет), foreignKey и relatedKey:
В какой-то другой модели прописана связь:
И использовать это дело можно как и обычную модель, только
Контроллеры VESP'а прекрасно справляются если указать массивом $primaryKey.
Единственное, на каких-то сложных Pivot еще не удалось потестировать, так что может быть какое-то непредвиденное поведение...
Василий Наумкин
16.08.2023, 17:24:19
Выглядит любопытно, надо будет поэксперементировать на досуге, спасибо.
Только непонятно, а как составить ключ из 3х колонок? У меня и такие модели есть. Полагаю, что никак.
NightRider
21.08.2023, 13:07:22
Вот такую ошибку пишет при попытке запуска сида.
Видимо в архиве с дампом такой таблицы не было. Можно дамп этой таблички?
Василий Наумкин
21.08.2023, 15:37:49
Сорян, что-то я уже второй раз затупил с этим дампом.
Держи правильный , в заметке про импорт юзеров тоже поменял.
ms2.sql.zip
2.93 MB, application/zip
Futuris
24.11.2023, 12:58:34
Да, все это очень интересно, но пока для меня разрабатывать на Vesp магазин с переносом данных из Modx неподъемно. Даже несмотря на то, что я уже пробежался по базе php и предыдущий курс прошел. Повторить все как попугай можно конечно. Но толк выйдет сомнительный.)
Зато Vesp мне нравится. Я по итогам предыдущего курса повесил его на свой VPS и сижу спокойно разбираюсь, пытаюсь простой сайт запилить. Но мне хочется научиться работать в докере: на локалке и далее - на сервере. В связи с этим вопрос. В репозитории vesp-shop имеется docker-production.yml , а в исходном VESP его нет. Но это же не значит, что нельзя скачать исходники Vesp, в локальном Докере создать сайт, и, как ты пишешь в конце курса - закинуть это все на рабочий сервер?
Василий Наумкин
24.11.2023, 13:22:58
Думаю, для многих так. Зато можно почитать и узнать всякое новое. Понять, что за пределами известного тебе есть еще много интересного, и стремиться к этому.
Да просто не дошли руки положить этот файл в общий репозиторий, но он должен подойти без проблем.
Я сам постоянно работаю с Vesp и предумываю что-то новое. Когда выпущу Орбиту, будет большое обновление Vesp, включая обновлённый конфиг Docker.
Futuris
24.11.2023, 13:46:14
Вне всякого сомнения. Для многих очень ценно, что ты делишься своими продвинутыми наработками. Думаю в свое время я вернусь к магазину, а пока займусь более осязаемыми вещами.
Futuris
24.11.2023, 14:55:16
Кстати а возможно впоследствии на готовом проекте обновлять версию Vesp?
Василий Наумкин
25.11.2023, 11:30:22
Vesp тянет 2 зависимости: vesp-frontent для фронта и vesp-core для бэкенда.
Их можно обновлять, но это не даст каких-то преимуществ само по себе. Просто ты сможешь использовать каие-то новые функции в своём приложении (если они были добавлены).
Обычно там просто исправление ошибок, которых сейчас очень мало, буквально раз-два в год. Текущий Vesp очень стабилен.
bezumkin.ru
Личный сайт Василия Наумкина
Прямой эфир
Василий Наумкин
23.12.2024, 05:33:00
В MODX сначала создали проблему, автоматически генерируя адреса, а потом "решили" заморозкой.
Так ч...
Дмитрий
14.12.2024, 09:10:38
Василий, прошу прощения, тупанул, не разобрался сразу. Фреймворк отличный! "Чистый лист" на vue, рис...
Василий Наумкин
05.12.2024, 20:01:14
В итоге основная ошибка была в неправильном общем root в Nginx, из-за чего запросы не улетали на фай...
Василий Наумкин
01.07.2024, 11:56:41
Да, верно, именно так.
А в контроллере, скорее всего, ловить данные методом post.
Василий Наумкин
26.06.2024, 09:38:15
О, точно, вылезает если не залогинен.
Спасибо, исправил!
Василий Наумкин
09.04.2024, 04:45:01
> Ошибка 500
Это не похоже на ошибку Nginx, это скорее всего ошибка PHP - надо смотреть его логи.
...
Уровни подписки
Спасибо!
500 ₽ в месяц
Эта подписка ничего не даёт, просто возможность сказать спасибо за мои заметки. Подписчики отмечаются зелёненьким цветом в комментариях.
Большое спасибо!
1 000 ₽ в месяц
И эта подписка не даёт ничего, кроме оранжевого цвета в комментариях и возможности сказать спасибо, но уже большое!