Создание блога

Дак он уже написан.
Хорошо. Может быть я неудачно выразился. У улучшения должна быть внятная цель. По последним сообщениям я не понимаю что за задачу ты решаешь в данный момент. Перекладывание кода в какой-то класс это не цель всё-таки, а средство.
Перечитывать всю тему из 15и страниц длиною в год я смысла не вижу. Опубликуй код на гитхабе, распиши roadmap с целями и в коментариях давай ссылки на отдельные коммиты или на PR.

Если тебе нужен слой поверх низкоуровневых запросов, логично назвать его "пользовательские сообщения" или "новостная лента", например. Никак не "запросы к модели post". ПМСМ так. В именовании классов и разделении зон ответственности исходи из задач предметной области, а не из атомарных действий.
 
Последнее редактирование:
У улучшения должна быть внятная цель. По последним сообщениям я не понимаю что за задачу ты решаешь в данный момент.

Перечитывать всю тему из 15и страниц длиною в год я смысла не вижу.
Дак и не надо, там страниц 10 наверно бесполезная возня с ванильным PHP.

Если тебе нужен слой поверх низкоуровневых запросов
Я что-то не понял про низкоуровневые запросы. У ларавел же ORM, квери билдеры.
 
Никак не "запросы к модели post". ПМСМ так. В именовании классов и разделении зон ответственности исходи из задач предметной области, а не из атомарных действий.
Почему так то плохо? Названия же отображают суть классов. Запросы к конкретной таблице.
 

Вложения

  • Screenshot from 2025-09-02 20-59-24.png
    Screenshot from 2025-09-02 20-59-24.png
    19,9 KB · Просмотры: 5
В именовании классов и разделении зон ответственности исходи из задач предметной области, а не из атомарных действий.
Почему так то плохо? Названия же отображают суть классов.
Влад Хононов - Изучаем DDD - предметно ориентированное программирование.
Какие из книг которые я вам рекомендовал вы уже прочитали? 😉
 
Ну, товарищ Валик, не все так просто. У меня же еще учеба и я еще изучаю Си. Поэтому у меня один вечер уходит на блог, а другой на Си. И не всегда еще есть время по вечерам. У меня было относительно свободное время в июле и августе. Но в это время я активно занимался фронтом для блога. Всю верстку и скрипты я делал сам, без всяких нейронок и готовых плагинов. На это тоже ушло много времени. Сейчас у меня опять мало времени. И если мне читать это DDD, то мне придется перестать писать код на пару месяцев, а то и больше. И документацию ларавел я еще не до конца дочитал. Когда прикажете читать это DDD?
 
Я ничего никому не приказываю, более того даже не уговариваю. Вы жалуетесь на нехватку времени, но не понимаете почему у вас его не хватает. А потому, что изобретаете велосипеды, вместо получения готовой информации проверенной сотнями тысяч читателей. В вашем случае перестать писать код и сесть за изучение принципов архитектуры самый идеальный вариант.
Моё сообщение написано в ноябре 24-го года, вот и представьте столько времени вы уже потеряли.
 
Лучше DDD оставь на потом, там опытные программисты ноги ломают, тем более с Laravel.
Но ты действительно очень-очень долго пишешь, за это время уже до тимлидов дорастают )
 
Лучше DDD оставь на потом, там опытные программисты ноги ломают, тем более с Laravel.
Ну вот.. Зря только первую главу прочитал (
Но ты действительно очень-очень долго пишешь
Не, ну можно как большинство ларавельщиков пихать все в контроллеры, везде юзать статику и ни о чем не думать. Взять эту Admin LTE уже сверстаную, на фронт тоже скачать какой-нибудь шаблон. Вместо нормальных скриптов притянуть жиквери и юзать готовые плагины. Тогда можно и за пол дня написать.
 
Лучше DDD оставь на потом, там опытные программисты ноги ломают
Я согласен, DDD сложная штука, особенно когда от теории переходишь к практике. На самом деле рекомендуемая книга это третья книга по DDD которуя я смог таки прочитать от начала до конца. Сначала был Вон Вернон "Предметно-ориентированное программирование. Самое основное". Читать трудно. Прочитал, но мало что понял возможно из-за того это выжимка. Потом была "синяя книга" Эванса. Бросил на половине, потому что при чнтении начинала жутко болеть голова. Вот где действительно вынос мозга. Некоторые рекомендуют читать именно в оригинале, так как перевод оставляет желать лучшего. Но дело немного в другом. Постоянные отвлечения от темы (ну зачем меня в середине книги пятисотый раз убеждать, что мне обязательно надо прочитать эту книгу). Постоянные перескоки вперёд-назад аля "а зачем это нужно мы узнаем в конце восьмой главы" хотя я читаю только третью. Поэтому Влад Хононов, где всё по порядку и с соответвующими картинками, лично для меня оказался гораздо более понятным. Эванса вряд ли я когда-нибудь снова возьмусь читать, а вот Вернона скорее всего пойду на второй заход.
 
Последнее редактирование:
Я снова забуксовал. Нигде не могу найти как реализуют поиск по сайту. На гитхабе все под копирку - один экшен, где просто ищут по какой-нибудь таблице (посты, юзеры). Нашел один трейт, но он очень страшный.

У меня сейчас такой класс, но ясное дело что он инвалидский. Первый экшен, это когда в инпут в форме пишут. Все остальные это когда нажимаешь на ссылку (для категорий, к примеру, <a href="http://localhost:8080/search/category/12" class="post__category">Бредские идеи</a>). Все экшены, кроме первого - одно сплошное дублирование, впрочем и первый тоже.
PHP:
final class SearchController extends Controller
{
    const ACTION_INDEX = 'public.search.index';
    const PER_PAGE = 5;

    public function __construct(
        protected ViewFactory $view
    ) {}

    public function posts(Request $request, PostQueries $postQueries, Redirector $redirect): View|RedirectResponse
    {
        if ($request->isNotFilled('search')) {
            return $redirect->back()->with('error', 'Введите поисковый запрос');
        }

        return $this->view->make(self::ACTION_INDEX, [
            'posts' => $postQueries->search($request->input('search'), self::PER_PAGE)
        ]);
    }

    public function user(User $user, UserQueries $userQueries): View
    {
        return $this->view->make(self::ACTION_INDEX, [
            'posts' => $userQueries->search($user, self::PER_PAGE)
        ]);
    }

    public function category(Category $category, CategoryQueries $categoryQueries): View
    {
        return $this->view->make(self::ACTION_INDEX, [
            'posts' => $categoryQueries->search($category, self::PER_PAGE)
        ]);
    }

    public function tag(Tag $tag, TagQueries $tagQueries): View
    {
        return $this->view->make(self::ACTION_INDEX, [
            'posts' => $tagQueries->search($tag, self::PER_PAGE)
        ]);
    }
}
Как быть? По идее нужно только два метода. Но если их будет два, то второй будет слишком раздутым, поскольку поиск по тегам, категориям, юзерам.
 
 
Я снова забуксовал. Нигде не могу найти как реализуют поиск по сайту. На гитхабе все под копирку - один экшен, где просто ищут по какой-нибудь таблице (посты, юзеры). Нашел один трейт, но он очень страшный.
Вот поэтому надо читать документацию полностью, есть готовое решение внутри самого ларавел https://laravel.com/docs/master/scout
 
Стоит ли мне применить шаблон Command Bus? У меня есть несколько классов, которые репозиторий или сервисный класс или бизнес логики или как их еще только не называют. Вот пример:
PHP:
final class PostFeed
{
    const PER_PAGE = 5;

    public function __construct(
        protected Post $model,
        protected Connection $db,
        protected Filesystem $storage,
        protected ImageUploader $uploader
    ) {}

    public function save(array $data): void
    {
        try {
            if (isset($data['image'])) {
                $data['image'] = $this->uploader->uploadPostImage($data['image']);
            }

            $tag_id = $data['tag_id'];
            unset($data['tag_id']);

            $this->db->beginTransaction();
            $post = $this->model->create($data);
            $post->tags()->attach($tag_id);
            $this->db->commit();
        } catch (\Throwable $e) {
            if (isset($data['image'])) {
                $this->storage->delete($data['image']);
            }

            $this->db->rollBack();
            throw new DbException("Ошибка создания поста: {$e->getMessage()}");
        }
    }

    public function update(Post $post, array $data): void
    {
        try {
            if (isset($data['image'])) {
                $data['image'] = $this->uploader->uploadPostImage($data['image']);
            }

            $oldImage = $post->image;
            $tag_id = $data['tag_id'];
            unset($data['tag_id']);
            $this->db->beginTransaction();
            $post->update($data);
            $post->tags()->sync($tag_id);
            $this->db->commit();

            if ($oldImage && isset($data['image'])) {
                $this->storage->delete($oldImage);
            }
        } catch (\Throwable $e) {
            if (isset($data['image'])) {
                $this->storage->delete($data['image']);
            }

            $this->db->rollBack();
            throw new DbException("Ошибка обновления поста: {$e->getMessage()}");
        }
    }

    public function delete(Post $post): void
    {
        try {
            $image = $post->image;

            $this->db->beginTransaction();
            $post->tags()->detach();
            $post->category()->dissociate();
            $post->delete();
            $this->db->commit();

            if ($image) {
                $this->storage->delete($image);
            }
        } catch (\Throwable $e) {
            $this->db->rollBack();
            throw new DbException("Ошибка удаления поста: {$e->getMessage()}");
        }
    }

    public function latestPaginated(): LengthAwarePaginator
    {
        return $this->model->orderBy('id', 'desc')->paginate(self::PER_PAGE);
    }

    public function count(): int
    {
        return $this->model->count();
    }
}
Тут конструктор принимает много зависимостей за раз, даже если не надо. Можно разбить каждый метод на отдельный класс, чтобы избавиться от этой проблемы. И сам класс с методами слишком раздутый.

Еще я узнал про такую штуку как DTO. И подумал, может мне вместо массфилинга вручную все сохранять, передавать через этот объект только то что нужно без всяких fillable, guarded? Я где-то читал, что в ООП нет места массивам.

Я просто не знаю, вроде обычный блог, с одной стороны, но с другой причины все таки есть.
 
Зачем тебе comand bus, какую проблему он здесь будет решать? разве что ты асинхронно захочpешь обрабатывать, но для этого есть очереди. Поэтому команд бас и убрали из ларавела.

Можешь использовать Dto, но опять же зачем? Не решит ли твою проблему распаковка.
PHP:
$post = $this->model->create(...$data);

function create($title, $text)
 
Дело в целесообразности и готовности инфроструктуры.
Например,было бы здорово, если бы форм рефвест в методе validated() возвращал dto и сами модели умели заполняться с dto/ Но сейчас в Laravel этот подход доставляет больше неудобств, чем работа с массивами, особенно с учётом фичи по развёртыванию ассоц массивов.
 
Я избавился от дублирования в контроллере для поиска!
Вместо этой ерунды:
PHP:
public function user(User $user, UserQueries $userQueries): View
{
    return $this->view->make(self::ACTION_INDEX, [
        'posts' => $userQueries->search($user, self::PER_PAGE)
    ]);
}

public function category(Category $category, CategoryQueries $categoryQueries): View
{
    return $this->view->make(self::ACTION_INDEX, [
        'posts' => $categoryQueries->search($category, self::PER_PAGE)
    ]);
}

public function tag(Tag $tag, TagQueries $tagQueries): View
{
    return $this->view->make(self::ACTION_INDEX, [
        'posts' => $tagQueries->search($tag, self::PER_PAGE)
    ]);
}
Теперь только два экшена и два маршрута. Первый - это когда форма сабмитится, там Scout работает, а второй - это когда по ссылкам нажимаешь:
PHP:
public function searchByLink(string $type, int $id): View
{
    $modelClass = self::modelsMap[$type];
    $model = (new $modelClass)->findOrFail($id);
    $posts = $model->posts()->orderBy('created_at', 'desc')->paginate(self::PER_PAGE);

    return $this->view->make(self::ACTION_INDEX, [
        'posts' => $posts
    ]);
}
PHP:
Route::group(['prefix' => 'search', 'as' => 'search.'], function () {
    Route::controller(PublicSearchController::class)->group(function () {
        Route::get('/posts', 'searchByForm')->name('form');
        //Route::get('/author/{user}', 'user')->name('user');
        //Route::get('/category/{category}', 'category')->name('category');
        //Route::get('/tag/{tag}', 'tag')->name('tag');
        Route::get('/{type}/{id}', 'searchByLink')->name('link');
    });
});
Не говно-код ли это? Всяко лучше же чем дублирование из трех экшенов.
 
Я никак не могу написать тест на удаление поста. В политиках стоит следующее:
PHP:
// class PostPolicy

public function before(User $user, $ability): bool
{
    if ($user->isAdmin()) {
        return true;
    }

    return false;
}

public function delete(User $user): bool
{
    return $user->isEditor();
}
Вот метод:
PHP:
public function testDeletePost(): void
{
    $post = Post::factory()->delete()->create();
    $this->actingAs($post->user);
    $response = $this->delete(route(self::ACTION_DELETE, $post->slug));
    $this->assertDatabaseMissing('posts', ['slug' => $post->slug]);
    $response->assertRedirect(route(self::ACTION_INDEX));
    $response->assertViewIs(self::ACTION_INDEX);
    $response->assertSessionHas('success', __('messages.post.deleted'));
}
Сама фабрика:
PHP:
class PostFactory extends Factory
{
    public function definition(): array
    {
        $user = User::factory()->create();
        $category = Category::factory()->create();

        return [
            'title' => $this->faker->sentence,
            'excerpt' => $this->faker->paragraph,
            'content' => $this->faker->text,
            'user_id' => $user->id,
            'category_id' => $category->id,
            'slug' => $this->faker->slug,
        ];
    }

    public function delete(): static
    {
        return $this->state(fn (array $attributes) => [
            'user_id' => User::factory()->createEditor()->create()->id
        ]);
    }
}
Тест пишет, что ничего не удалилось:
Код:
   FAILED  Tests\Feature\Public\PostTest > delete post                                                                                                         
  Failed asserting that a row in the table [posts] does not match the attributes {
    "slug": "impedit-facere-culpa-rerum-iste-et-molestiae-doloribus"
}.

Found similar results: [
    {
        "slug": "impedit-facere-culpa-rerum-iste-et-molestiae-doloribus"
    }
].

Что не так то? Само удаление по факту работает, я тестил и от админа и от модератора, у других и кнопки нет. Делал дамп в самом тесте, там все правильно отображает, роль модератора (editor).
 
Я вообще не знаю что делать... Политики ни в какую не хотят проходить тесты.
PHP:
public function edit(User $user, Post $post): bool
{
    return $user->id == $post->user_id;
}
Простая проверка. Она всегда true. Но тест пишет, что 403, запрещено! Карл! Даже на скрине видно, что id совпадают.

Screenshot from 2025-10-02 20-58-50.png
Если из маршрута:
PHP:
Route::get('/edit/{post:slug}', 'edit')->name('edit')->can('edit', Post::class);
убрать этот middleware "can", то все работает. В какую сторону копать? В гугле и на гитхабе вообще ничего не нашел насчет политик и тестов.
 
Сверху