• Как валидировать данные входящие в объект при десериализации в Symfony?

    tommy-vercetti
    @tommy-vercetti
    Symfony/Golang
    Пример кода из моего древного проекта. Суть была такой, что если в api-эндпоинт для \DateTime и array передать некорректные типы, то денормалайзер кидал эксепшн Symfony\Component\Serializer\Exception\ExceptionInterface (не удалось создать дату из строки "blabla").
    Чтобы этого избежать, я передаю в денормалайзер кастомную константу Context::REQUEST_DENORMALIZATION и отлавливаю этот эксепшн в RequestArrayDenormalizer и RequestDateTimeNormalizer.Там смотрю есть ли эта константа, если есть, то значит у меня денормалазайзер был вызван для реквеста. Дальше я возвращаю это значение как есть, чтобы валидатор его смог провалидировать.

    namespace App\Application\Serializer\Denormalizer;
    
    use App\Application\Serializer\Normalizer\Context;
    use App\Application\Util\Helper\Arr;
    use Symfony\Component\Serializer\Normalizer\AbstractObjectNormalizer;
    use Symfony\Component\Serializer\Normalizer\DenormalizerInterface;
    
    class RequestDtoDenormalizer implements DtoDenormalizerInterface
    {
        private DenormalizerInterface $denormalizer;
    
        public function __construct(DenormalizerInterface $denormalizer)
        {
            $this->denormalizer = $denormalizer;
        }
    
        public function denormalize(array $input, string $className): object
        {
            $input = Arr::mapRecursive($input, fn ($value) => is_string($value) ? trim($value) : $value);
    
            return $this->denormalizer->denormalize($input, $className, null, [
                Context::REQUEST_DENORMALIZATION => true,
                AbstractObjectNormalizer::DISABLE_TYPE_ENFORCEMENT => true,
            ]);
        }
    }

    namespace App\Application\Serializer\Normalizer\Http;
    
    use App\Application\Serializer\Normalizer\Traits\AvoidExceptionForRequestTrait;
    use Symfony\Component\Serializer\Normalizer\ArrayDenormalizer;
    
    class RequestArrayDenormalizer extends ArrayDenormalizer
    {
        use AvoidExceptionForRequestTrait;
    }

    namespace App\Application\Serializer\Normalizer\Http;
    
    use App\Application\Serializer\Normalizer\Traits\AvoidExceptionForRequestTrait;
    use Symfony\Component\Serializer\Normalizer\DateTimeNormalizer;
    
    class RequestDateTimeNormalizer extends DateTimeNormalizer
    {
        use AvoidExceptionForRequestTrait;
    }

    namespace App\Application\Serializer\Normalizer\Traits;
    
    use App\Application\Serializer\Normalizer\Context;
    use Symfony\Component\Serializer\Exception\ExceptionInterface;
    
    trait AvoidExceptionForRequestTrait
    {
        /**
         * {@inheritDoc}
         */
        public function denormalize($data, $type, $format = null, array $context = [])
        {
            try {
                return parent::denormalize($data, $type, $format, $context);
            } catch (ExceptionInterface $e) {
                if (!isset($context[Context::REQUEST_DENORMALIZATION])) {
                    throw $e;
                }
    
                return $this->transform($data);
            }
        }
    
        /**
         * @param mixed $data
         * @return mixed
         */
        protected function transform($data)
        {
            return $data;
        }
    }

    namespace App\Application\Serializer\Normalizer;
    
    abstract class Context
    {
        public const REQUEST_DENORMALIZATION = 'request_denormalization';
    }
    Ответ написан
  • Выбор ОС для разработки под Docker: Windows или Linux?

    tommy-vercetti
    @tommy-vercetti
    Symfony/Golang
    Сам разрабатывал на Windows под докером, честно, на Linux (Ubuntu) все намного проще поднять. Есть нормальный терминал, не надо заморачиваться с шарингом файлов между хост-машиной и докером (в отличии от Windows и MacOS), xDebug и всякие штуки с полпинка заводятся. На Windows мне приходилось вечно париться: то одно, то другое не работает.

    Согласен, что у Linux User Experience не тот. Вроде, на последней Ubuntu 20.04 всё довольно неплохо выглядит. Хотя что я говорю, Windows 10 - эталон неконсистентного дизайна: одни части интерфейса из 95-ой версии тянутся, другие - мешанина Win8 с 10.

    Недавно вышел WSL2, можно попытаться на нем разрабатывать с Docker. Если зайдет и не будет проблем, не вижу смысла ставить Linux.

    P.S. Чтобы привыкнуть к той же Ubuntu я снес Windows. И 2 года сидел с Ubuntu до покупки новой рабочей машинки. Сейчас у меня 2 системы: Windows - для игр, учебы и потребления контента, Ubuntu - чисто для разработки.
    Ответ написан
  • Какой взять macbook в 2020 для веб-программирования?

    tommy-vercetti
    @tommy-vercetti
    Symfony/Golang
    Я бы не советовал брать MacBook за такие деньги, надо смотреть версию с 16 гигами ОЗУ. Уж лучше взять ноут на Windows.
    Неделю назад я купил сестренке ноут MSI Modern 14 A10M: i5 10210U, SSD 512 Gb, RAM 8 Gb (можно до 32 Gb расширить), экран FullHD IPS со 100% охватом sRGB. Со всякими бонусами вышло в рублях 52к.
    Ответ написан
  • Symfony 3 как при вызове класса передать в него параметры без new?

    tommy-vercetti
    @tommy-vercetti
    Symfony/Golang
    Для SF 3.4 https://symfony.com/doc/3.4/service_container/fact...

    // src/AppBundle/Service/MpdfFactory.php
    namespace AppBundle\Service;
    
    use Mpdf\Mpdf;
    
    class MpdfFactory
    {
        public static function createMpdf(): Mpdf
        {
            return new Mpdf(['tempDir' => self::getTmpDir(), 'utf-8', 'A4-P']);
        }
    }

    # app/config/services.yml
    services:
        Mpdf\Mpdf:
            # call the static method that creates the object
            factory: ['AppBundle\Service\MpdfFactory', 'createMpdf']
            # define the class of the created object
            class: Mpdf\Mpdf

    Далее просто в конструкторе сервиса указываете \Mpdf\Mpdf.
    Ответ написан
  • Битрикс как первая работа?

    tommy-vercetti
    @tommy-vercetti
    Symfony/Golang
    Если больше никуда не берут, то можно.

    Моя первая работа тоже была связана с Битриксом: попрактиковал JS, PHP и научился взаимодействовать с коллегами/заказчиками. Через полгода ушел в другую контору писать на нормальном фреймворке.
    Ответ написан
  • Программирование деформирует человека как личность?

    tommy-vercetti
    @tommy-vercetti
    Symfony/Golang
    Мне тоже 28 лет и похожая ситуация.

    Проблемы с ЖКТ такие, что пришлось перевестись на удаленную работу и уже 3 года все никак не удается вылечиться. Прошел кучу врачей, все разводили руками и назначали препараты, которые снимают только симптомы.
    На фоне этого у меня была апатия и не было желания вообще что-либо делать. Позже сам узнал, что у меня паразиты в печени и желчном (съел рыбу семейства карповых) и все это вылезло после операции, т.к организм был ослаблен.

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

    Так что ваше состояние также может быть связано с проблемами ЖКТ. Ухудшается всасываемость веществ из-за этого появляется раздражительность, апатия, сонливость. Рекомендую сделать узи брюшной полости + почки, дуоденальное зондирование (из-за сидячего образа жизни начинается застой желчи), ФГДС, общий анализ крови и биохимию крови.

    Ну и стараться выходить на улицу как можно чаще, путешествовать и не проводить досуг в виртуальном мире, если это не касается прокачивания скиллов.
    Ответ написан
  • Что может дать изучение Symfony разработчику Laravel?

    tommy-vercetti
    @tommy-vercetti
    Symfony/Golang
    Symfony не загоняет в жесткие рамки разработчика и на нем точно также можно писать лапшу, как и на Laravel.

    Мне 3 недели назад достался довольно успешный европейский стартап, написанный на Symfony. Открыл код и ужаснулся:
    • Нет никакого SOLID, вся логика находится в контроллерах вместе с валидацией. И валидация делается не через компонент Validator, а ручками через empty(), isset() и тд.
    • Какие-то костыльные и кастомные события и тд.
    • Нет брокеров сообщений и из-за этого задержка в эндпоинтах доходит до 5-ти секунд.
    • Очень медленные тесты, 4709 assertions проходили за 54 минуты на локальной машине с i7 10-го поколения. За пару дней мне удалось сократить это время до 70 секунд. Теперь CI/CD очень быстрый и CTO вне себя от счастья.

    И на Laravel я видел крутые с точки зрения кода проекты, в которых было приятно участвовать. Там есть тот же DI, ивенты/события, можно интегрировать Doctrine если сильно хочется. Не хочешь фасады, юзай контракты. Вот даже пример есть Laravel-проекта с DDD.
    Ответ написан
  • Как запустить LEMP на wsl2?

    tommy-vercetti
    @tommy-vercetti
    Symfony/Golang
    Пробовал WSL 2 с Docker, всё дико лагает если проект лежит в Windows. Пример, получить список команд через bin/console в Symfony нужно ждать секунд 5.

    Желательно проект хранить внутри WSL и открывать его через сетевую папку \\wsl$ в PHPStorm либо с помощью VSCode. По скорости все почти на уровне нативной Linux-системы. Из проблем: мне не удалось завести xDebug в PHPStorm через WSL с Docker. Для меня оказалось проще накатить Ubuntu второй системой.

    Если хотите шаринг папок с Windows, то WSL 1 пока подходит лучше. Производительность, конечно, не такая как на нативной системе, но работать можно.
    Ответ написан
  • Как правильно деплоить laravel проект?

    tommy-vercetti
    @tommy-vercetti
    Symfony/Golang
    Т.к вы не работали с системами контроля версий и не сталкивались с деплоем, то я не буду вам советовать CI/CD с различными ветками и тд. Вам сходу будет сложно разобраться.

    Предлагаю для начала более простой способ:

    Если проект коммерческий, то создаем приватный репозиторий в каком-то сервисе (к примеру, GitHub).
    Коммитим (фиксируем) изменения и пушим их в репозиторий.
    Контроль версий нам нужен для того, чтобы хранить историю изменений проекта, делать откаты в случае ошибки и тд.

    У Laravel есть пакет Laravel Envoy. В файле Envoy.blade.php описывыем список команд что нужно сделать на сервере, подключившись по SSH.
    @task('production:deploy', ['on' => 'production'])
        cd /var/www/my-project-folder
    
        git pull origin master // подтягиваем изменения из репозитория
    
        php composer install --no-dev --optimize-autoloader
    
        php artisan migrate --force // запуск миграций
    @endtask

    И локально (на своем ПК) запускаем таску envoy run production:deploy. Envoy подключится по SSH к серверу и запустит там эти команды.

    Про работу с базамы данных:

    Должно быть несколько БД: локальная и продакшн.
    Допустим, я хочу добавить новое поле в БД. Для этого локально создаю миграцию (это обычный php-класс), описываю какое поле добавить и запускаю её. Коммичу изменения в git и делаю пуш в репозиторий. И запускаю таску envoy run production:deploy, а там у нас уже есть команда для миграций.
    Для генерации тестовых данных для локальной бд можно использовать сиды.
    Ответ написан
  • Как автоматически запустить сервис?

    tommy-vercetti
    @tommy-vercetti
    Symfony/Golang
    В Symfony аутентификация делается через Custom Authentication System with Guard или Custom User Provider

    Рендеры глобальных блоков типа менюшек и тд делается с помощью Embedding Controllers
    <div id="sidebar">
        {# if you don't want to expose the controller with a public URL,
           use the controller() function to define the controller to execute #}
        {{ render(controller(
            'App\\Controller\\BlogController::recentArticles', {max: 3}
        )) }}
    </div>

    P.S. Не юзайте глобальные переменные типа $_GET, $_POST и тд, не тащите свой багаж знаний из эпохи динозавров. Уже давно придумали Request объект. Есть документация по фреймворку и Symfonycasts, можно выделить выходные и пройти основной курс по Symfony.
    Ответ написан
  • Стоит ли покупать онлайн курсы?

    tommy-vercetti
    @tommy-vercetti
    Symfony/Golang
    Лучше взять подписку у хекслета за $39 в месяц https://ru.hexlet.io/professions/php

    Если нужен еще и HTML и CSS, то идем на htmlacademy. Разделы "Знакомство с HTML и CSS" и "Продвинутый HTML и CSS". На первое время бесплатного аккаунта хватит. Если нужен платный функционал, то надо брать подписку за 590руб. Только стоит быть осторожным, они любят впаривать курсы за 11к.

    Далее идете на Laracasts или Symfonycasts в зависимости от того какой вам фрейморк интересен. На одном популярном торрент-трекере есть уже раздачи по этим фреймворкам. Но если деньги есть, то лучше, конечно, взять подписку.
    Ответ написан
  • Как сделать груповую проверку форму в Symfony 4.4.8?

    tommy-vercetti
    @tommy-vercetti
    Symfony/Golang
    Из доки по Symfony

    As with most of the other constraints, null and empty strings are considered valid values. This is to allow them to be optional values. If the value is mandatory, a common solution is to combine this constraint with NotBlank.

    Т.е валидация не сработает, когда прилетит null или пустая строка - это дефолтное поведение.

    https://symfony.com/doc/current/reference/constrai...
    Ответ написан
  • Где лучше посмотреть реальные кейсы использования RabbitMQ с примерами кода, в идеале на GO или PHP?

    tommy-vercetti
    @tommy-vercetti
    Symfony/Golang
    Реальные кейсы использования можно посмотреть только на "реальных" проектах. Никто не выкладывает коммерческие проекты на всеобщее обозрение. Мы используем RabbitMQ с компонентом Symfony Messenger для следующих сценариев.

    • SMS/Email сообщения
    • Кропы фото. Сначала фотку загружаем в сторадж GCS. Затем отправляем сообщение в RabbitMQ, где мы из GCS скачиваем на локальный диск, кропим и грузим обратно GCS.
    • Удаление старых файлов: фото, pdf-ки и тд при изменении профайла или при перегенерации инвойса
    • Тяжелые операции, которые могут блокировать респонс. Различные обращения к сторонним API: создание профиля в платежной системе, в salesforce при регистрации.

    Вот простой пример. Мы создаем сущность миссии и рассылаем уведомления менеджерам и part-time employee, находящимся недалеко от места создания миссии. Далее у нас есть 2 воркера (async_new_mission_area_notification_to_manager и async_new_mission_notification_to_part_time_employee), которые слушают это событие.

    namespace App\Domain\Mission;
    
    class Mission implements ToArrayTransformable, RaiseEventsInterface
    {
        public function __construct(/* ... */)
        {
            // ...
    
            $this->initUuid();
    
            $this->raise(new MissionCreatedEvent($this->getUuidString()));
        }
    }
    
    namespace App\Application\Bus\MessageHandler\Event\Mission;
    
    class SendNewMissionAreaNotificationToManager implements MessageSubscriberInterface
    {
        private MissionRepositoryInterface $missionRepository;
    
        public function __construct(MissionRepositoryInterface $missionRepository)
        {
            $this->missionRepository = $missionRepository;
        }
    
        public function __invoke(MissionCreatedEvent $event): void
        {
            $mission = $this->missionRepository->getOneByCriteria(new MatchingUuidCriteria($event->getMissionUuid()));
    
            if (!$mission instanceof Mission) {
                return;
            }
    
            // Здесь уже ищем нужных менеджеров и рассылаем им уведомления
        }
    
        public static function getHandledMessages(): iterable
        {
            yield MissionCreatedEvent::class => [
                'from_transport' => 'async_new_mission_area_notification_to_manager',
            ];
        }
    }
    
    namespace App\Application\Bus\MessageHandler\Event\Mission;
    
    class SendNewMissionNotificationToPartTimeEmployeeHandler implements MessageSubscriberInterface
    {
        private MissionRepositoryInterface $missionRepository;
    
        public function __construct(MissionRepositoryInterface $missionRepository)
        {
            $this->missionRepository = $missionRepository;
        }
    
        public function __invoke(MissionCreatedEvent $event): void
        {
            $mission = $this->missionRepository->getOneByCriteria(new MatchingUuidCriteria($event->getMissionUuid()));
    
            if (!$mission instanceof Mission) {
                return;
            }
    
            // Рассылаем уведомления подходящим part-time employee
        }
    
        public static function getHandledMessages(): iterable
        {
            yield MissionCreatedEvent::class => [
                'from_transport' => 'async_new_mission_notification_to_part_time_employee',
            ];
        }
    }

    Если не заходит Symfony Messenger, то могу посоветовать SwarrotBundle с odolbeau/rabbit-mq-admin-toolkit.
    Ответ написан
  • Правильно ли использовать UTC везде на сервере?

    tommy-vercetti
    @tommy-vercetti
    Symfony/Golang
    Уже поздно отвечать, но напишу как мы работаем с датами в API на Symfony. Вдруг пригодится кому-нибудь.

    Даты храним в UTC в БД и таймзона для PHP выставлена в UTC.
    На фронт отдаем даты в ISO8601 - 2020-05-03T13:00:00.000Z и он спокойно с помощью moment.js переводит в текущее время юзера.

    Фронт присылает даты со смещением 2020-05-03T15:00:00+02:00, мы это перегоняем в UTC и сохраняем в БД.

    В сущности юзера храним таймзону (например, Europe/Paris) и юзеры в профиле могут её сменить. В twig-шаблонах для Email и SMS используем фильтры, которые переводят в таймзону юзера:

    {{ mission.range.start | to_user_timezone(employee) }}

    В MySQL используем функцию CONVERT_TZ в запросах, где нам нужно подбирать миссию в зависимости от расписания юзера. Юзер у себя в профиле указывает время когда он доступен, например, 15:00-20:00.
    Ответ написан
  • Где можно посмотреть толковые реализации API на Symfony?

    tommy-vercetti
    @tommy-vercetti Автор вопроса
    Symfony/Golang
    Опишу тут к чему мы в итоге пришли, вдруг кому-нибудь пригодится :)

    - Используем RDM (Rich Domain Model)
    - Ивенты бросаем внутри сущности. Для этого мы используем интерфейс RaiseEventsInterface и трейт RaiseEventsTrait.

    interface RaiseEventsInterface
    {
        public function releaseAndResetEvents(): array;
    }
    
    trait RaiseEventsTrait
    {
        protected array $events = [];
    
        public function releaseAndResetEvents(): array
        {
            $events = $this->events;
    
            $this->events = [];
    
            return $events;
        }
    
        protected function raise(AbstractDomainEvent $event): void
        {
            $event->initTime();
    
            $this->events[] = $event;
        }
    }

    В DoctrineDomainEventPublisher мы пробегаемся по всем новым и измененным сущностям и забираем ивенты. Далее сортируем эти ивенты по времени и диспатчим в Event Bus (компонент symfony/messenger) .

    - Не используем бандлы: FosRestBundle, FosUserBundle

    - Для валидации используем DTO через action argument resolver. В основном валидация через DTO делает простые проверки: на тип, дату, период времени и тд. Проверки на принадлежность к какой-то сущности проводятся в самой сущности. В случае невалидного состояния из сущности бросается эксепшн, например UncancellableMissionException, MissionOfferSlotCannotBeAcceptedException и тд.

    - В контроллере нет бизнес-логики. Сообщение диспатчится в Command или Query Bus.

    - Для репозиториев используем интерфейсы с общими методами: get, getOneByCriteria, add и тд. Например, метод add делает внутри $em->persist().

    - Стараемся избегать entity manager в коде типа: $em->persist(), $em->flush(). Благо компонент symfony/messenger предоставляет doctrine_transaction middleware. Конечно, не везде удается обходиться без entity manager, например, в ситуации с блокировками транзакций.

    - Для генерации доки используем API Blueprint - пакет https://github.com/bukalapak/snowboard

    - Для респонсов используем Fractal https://fractal.thephpleague.com/transformers/

    - Для тестов API - Behat
    Ответ написан
  • Правильно ли выбрасывать исключения в бизнес логике?

    tommy-vercetti
    @tommy-vercetti
    Symfony/Golang
    Да, правильно.

    Исключения должны бросаться, когда обработка сценария невозможна по каким-то причинам: прилетел некорректный объект или сама сущность не может быть изменена и тд.

    Пример кода на моем проекте:
    namespace App\Domain\Mission;
    
    class MissionOffer
    {
        // ...
    
        public function __construct(Employee $employee, Mission $mission)
        {
            if ($employee->worksInShop($mission->getShop())) {
                throw new MissionOfferCannotBeCreatedForEmployeeException(
                    $employee->getUuidString(),
                    $mission->getUuidString()
                );
            }
    
            $this->initUuid();
    
            // ...
        }
    
        // ...
    
        private function accept(): void
        {
            if (self::STATUS_OFFERED !== $this->status) {
                throw new MissionOfferCannotBeAcceptedException();
            }
    
            $this->status = self::STATUS_ACCEPTED;
            $this->acceptedAt = new DateTime();
        }
    
        public function refuse(): void
        {
            if (self::STATUS_OFFERED !== $this->status) {
                throw new MissionOfferCannotBeRefusedException();
            }
    
            $this->status = self::STATUS_REFUSED;
            $this->refusedAt = new DateTime();
    
            foreach ($this->offerSlots as $offerSlot) {
                if ($offerSlot->isPending()) {
                    $offerSlot->refuse();
                }
            }
        }
    }

    Эти исключения ловятся в одном месте - exception subscriber'е. В зависимости от эксепшена возвращается статус код + тело ответа.
    Ответ написан
  • Сколько у кого держит батарея iPhone на ios 13?

    tommy-vercetti
    @tommy-vercetti
    Symfony/Golang
    Совет: не обновляться на новую iOS пока не выйдут *.3 или *.4 версии. Иначе придется страдать: в лучшем случае будут подтормаживания, в худшем - перегревы и умирающий аккумулятор.

    Как вариант можно отключить:
    • Screen Time
    • Шаринг аналитики
    • Трекинг рекламы (Настройки -> Конфиденциальность -> Реклама -> Ограничить трекинг рекламы)
    • Ненужные службы в отслеживании устройства (Настройки -> Конфиденциальность -> Службы геолокации -> Системные службы).
    • Фоновое обновление приложений

    P.S. Сам просидел на iOS 10 на 6S с осени 2016 по июнь 2019. Успешно миновав самую тормозную 11-ю версию, обновился на 12.4.1.
    На iPhone 11 пока сижу на 13.1.2. Боюсь обновляться до последней 13.2.3, т.к говорят, что начиная с 13.2 сделали хуже с многозадачностью.
    Ответ написан