Может войдёшь?
Черновики Написать статью Профиль
nailfor

nailfor +21

Вступил в наши ряды: 9 января 2019

Замечен в последний раз: 14 апреля 2024

Оставил на форуме: 40 сообщений

Последнее сообщение: 14 декабря 2020

Вы сможете отправить письмо, если войдёте

Статьи (2)

Солидные практики

BestPractics SOLID

В современном искусстве программирования техника SOLID получила широкое распространение благодаря старому доброму принципу «разделяй и властвуй».
В этой статье я хочу осветить некоторые солидные моменты, которые неочевидны новичкам.

Для начала я покажу общую архитектуру приложения, так как без нее будет непонятно использование классов и наследований.

Так как я буду использовать ресур-контроллер, то маршруты описываются примитивным массивом вида

PHP
<?php

Route
::resources([
    

Download url with self-signed HTTPS

В документации по Laravel тут и его английском варианте весьма неявно указано, как производить загрузку внешних файлов.

Цитирую оригинал

The put method may be used to store raw file contents on a disk. You may also pass a PHP resource to the PHPput method, which will use Flysystem's underlying stream support. Using streams is greatly recommended when dealing with large files:

Комментарии (7)

nailfor

«В Вашем примере, видимо, UserRepository будет иметь методы addAddress($userId, $address), getAddress ($userId), saveUser($user, $address, $photos) или getUser($id, $fields) — чтобы получить пользователя с зависимыми сущностями»

ок. Допустим. Я лишь хочу напомнить, что Пользователь — это актор. На него примерно 99.9% всего завязано, при таком подходе в UserRepository будет чуть менее чем всё, что касается пользователя(а его касается вообще всё) и поверьте, я видел такой трындец, получался классический God-class который назывался или UserController или UserRepository, не важно.

И Вы правильно подметили, что ActiveRecord — это уже абстракция которая уже содержит необходимое разделение, если так уж хочется иметь слои, а не модели — всегда можно вынести запросы в билдер, при этом остается гибкость композиции и нет мешанины методов. Тогда логику работы с моделью/билдером можно вынести в сервис, связи таблиц, если не требуется запись, можно сделать в релейшенах модели и использовать with, а вот уже для записи создать отдельный сервис.

nailfor

CSR — одно из самых неудачных архитектурных решений со времен Б4.

Предлагается для каждой модели написать свой репозиторий и сервис к нему. Но, простите, а что делать, если, например, одной модели оказывается недостаточно? К примеру пользователю понадобился адрес, его куда прикажете запихивать? В UserRepository? Или уже в AddressRepository?
Если в последний, то как передать пользователя? Через id? Тогда будет N+1.
Через ids? Тогда что делать, если сам UserRepository используется в каком-нибудь CustomerService который, не к ночи будет сказано, подтягивает еще какой-нибудь OrderRepositiory?
Т.е. дальше одного шага проблема не решается принципиально, придется городить какие-то обходы выборок, сбор ids, передача всего этого в репы... и всё ради чего???


PHP
try {
            
$user $service->getUser($request->getUserId());
        }

Изюмительно. Я прямо сижу.. и старческие слезы умиления наворачиваются на глазах. Простите, а что Вы собираетесь делать, когда в запрос нужно будет передать новый параметр, кроме id? Ну, например, СНИЛС или что-то еще?
Давайте я Вам помогу. Небольшой реверс-энжинеринг.
Так как getUserId — это какой то малопонятный огород, вангую, что это обычная обертка над Request за каким-то бесом засунутая в виде метода в ShowUserRequest

PHP
protected int $userId;
public function 
__construct()
{
    
$this->userId = (int) $this->id;
}

public function 
getUserId(): int
{
    return 
$this->userId
}

Зачем? Да просто, ради писанины. Ведь теперь, чтобы добавить новый параметр и передать его в сервис нужно еще методов в ShowUserRequest закинуть.
Почему нельзя просто сделать (int) $request->get('id', 0) ? Вопрос чисто риторический.

Ну оке.. с ид понятно, добавление всего одного поля в такую систему приводит к каскаду правок в
1. реквесте
2. контроллере
3. сервисе(он же получает не реквест, а параметры отдельно)
4. репозитории(его же нужно как то обработать)
Несколько многовато для такого простого действия.

PHP
catch (UserNotFoundException $exception) {
            return 
view('user.not_found', ['user_id' => $request->getUserId()]);
        }

тут еще интереснее. Вместо совершенно логичного и общего для любых моделей ModelNotFound требуется на каждый реп описать свой эксепшен. За ради что?
Т.е. в репе User::find($userId) заменить на User::findOrFail($userId) — уже не православно, а наогородить

PHP
if ($user === null) {
            throw new 
UserNotFoundException("User {$userId} not found.');
        }

— это феншуй. Ню... токооое.


«Негативные последствия такой реализации в том, что при необходимости получения разных ответов в разных методах контроллеров, одно и то же исключение может быть преобразовано по разному.»

Это в каком мире 404 страница должна быть преобразована по разному? Нет модели — пульнул одну для всех 404 и забыл. Я так понимаю, что разные ответы нужны чтобы программисты не скучали.


«В теле метода сервиса не выполняются запросы к базе данных. Только бизнесовая логика»
и это приводит к тому, что запросы выполняются катастрофически долго, а логика превращается в спагетти-стайл с вызовом сервисов внутри других сервисов обернутое циклами внутри других циклов со вложенностью 5 и более и нытьём что ой у нас все как то сложно получилось давайте перейдем на DDD там всё еще сложнее.


«Репозитории — классы, отвечающие за сохранение и извлечение некоторого набора данных»
Нет конечно. Ничего подобного репозитории НЕ ДЕЛАЮТ. Этим занимается DAO! Запомни, а лучше запиши и никогда в дальнейшем не путай!!!
Репозиторий — это высокоуровневая абстракция доступа к данным. Никаких изменений он делать не может и не умеет.


«В идеале репозиторий не должен возвращать или принимать объекты класса Model»
В сферическом вакууме, надо понимать. Там, где сферические кони.
Паттерн был придуман задолго до того, как появилась эта ваша богомерзкая DDD и предназначался, в первую очередь, для быстрого доступа к уже полученным данным. Возможно ты в курсе, что есть языки, которые не выгружаются из памяти всякий раз на каждом запросе, так вот в них(например в яве) репы очень комфортно себя чувствуют, так как являются эдаким кэшем справочника в памяти, ну и, разумеется, хранят они самые что ни на есть Models, а не эти ваши «энтити», простихоспиди.


«Лучше создать собственные entity или dto.»
Ага.. давай еще один огород сверху прикостылим. А то там абстракций чет маловато, ОРМ уже не хватает. Теперь для добавления одного поля мало того, что нужно будет сделать миграцию и добавить логику в сервис, так еще написать пару-десятков методов гетеров и сетеров в entity, dto, valueObject commandBus и всё заверте... но это не точно.


«Но с опытом вы поймете...»
что простое лучше чем сложное, что код, который никогда не ломается — это код, которого нет. Его проще сопровождать, проще добавлять новые фичи, проще разбираться с багами. YAGNI + KISS.

nailfor

— Зачем связывать репозиторий с request из веба?

реп может обрабатывать любые запросы, в том числе и запросы с файлами.

— Зачем репозиторий работает с фасадом модели? Почему бы явно не прокинуть билдер?
По той же причине. Реп — универсален, он может работать с любыми моделями, использования билдера привело бы к определению какого-то единого общего интерфейса, который далеко не всегда возможен.

— Что такое Ajax?
Аjax — подход к построению интерактивных пользовательских интерфейсов веб-приложений, заключающийся в «фоновом» обмене данными браузера с веб-сервером(c) вики.

— Вы на ajax, веб будете делать новый репозиторий?
Естественно. SOLID как раз об этом.

— переменные без доллара
очепятка. Бывает.

— Придет момент, когда нужно запретить удаление модели
В контроллере есть
protected $cannt = [
'destroy',
]
в статье не описано для упрощения.

— От ифа в CustomersRepository Вы ведь так и не избавились
на самом деле избавился. Переменная в контроллере содержит имя переменной реквеста, которая подключает разные репозитарии, в зависимости. Опять же здесь для простоты это пропущено.

nailfor

да еще момент с самой задачей...
Допустим задача что то там делает и по какой то причине решила, что не пришло ее время. Что делать?
Правильно — отложить задачу на потом.
Сделать это можно так

public function handle()
{
...
$this->release(self::REPEAT_COOLDOWN);
}

ну а если сделать просто return без release то задача завершится и уйдет из очереди.
Так же несколько полезных команд:
./artisan queue:work --queue=имя_очереди — запустит слушатель на конкретную очередь
./artisan queue:work --timeout=0 — выполнять задачу без ограничений во времени(по умолчанию дается 60сек, если задача не уложилась — она фейлится)
ну и конечно же
./artisan queue:work --help ))

nailfor

ноу бэд.. однака дополню...

2. Очередь — первые шаги

dispatch($emailJob);
в контроллере лучше $this->dispatch(..) а в произвольно модели добавить трейт Dispatchable

3. Драйвер очереди database

официальная документация настоятельно как бэ намекает использовать Redis, но в нем имеется один подводный камень, который может запросто утянуть на дно, а именно — память.
В идеально сферическом вакууме(там где кони, разумеется) задача в очереди должна практически сразу обрабатываться воркером, но что если..
1. произошла НЕХ и задача не успела выполнится, когда в очередь упала другая задача?
2. произошла НЕХ и задачи вообще перестали выполнятся
3. ...
4. ТЫДЫЩ: Сервер лег
Почему? Потому что Redis — это память. Рано или поздно она закончится и система ляжет.

Второй проблемой является весьма странное поведение Laravel воркера — он обрабатывает одну и ту же(!!! Да, Карл, очереди тут такие очереди) задачу разными инстансами. Т.е. бесполезно запускать 10-ок воркеров одной очереди — они все получат одну и ту же задачу.
Решение, конечно же есть, например блокировки: https://github.com/ph4r05/laravel-queue-database-ph4

Далее.. database бывает разный... Я, например, использую mongodb от Jenssegers.
К несчастью у автора нет конфига настроки очереди в манах, а гугление приводит на его же страницу issue, где ни ответа ни привета. Однако такой конфиг удалось найти в тестах, для страждущих вот он:
'database' ⇒ [
'driver' ⇒ 'mongodb',
'table' ⇒ 'queues',
'queue' ⇒ 'default',
'retry_after' ⇒ 90,
'connection' ⇒ 'mongodb',
],

nailfor

Мысль у меня возникает только одна "Дядя Вова, ты..?" В смысле нафига??? И доколе??? php-fpm, не?