-
Notifications
You must be signed in to change notification settings - Fork 0
Задание 0 #1
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Задание 0 #1
Conversation
It's isnt work because memory leak in DebugClassLoader and AnnotationDriver
|
На что именно хочет получить фидбэк? Вопросы ревьюерам |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Надеюсь ответил на заданные тобой вопросы и чуток прояснил что к чему
| # * .env.$APP_ENV committed environment-specific defaults | ||
| # * .env.$APP_ENV.local uncommitted environment-specific overrides | ||
| # | ||
| # Real environment variables win over .env files. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Комментарии пакетов и сгенерированный текст желательно бы убирать.
| @@ -0,0 +1,73 @@ | |||
| <?xml version="1.0" encoding="UTF-8"?> | |||
| <module type="WEB_MODULE" version="4"> | |||
| <component name="NewModuleRootManager"> | |||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Понимаю, что папочку с конфигом phpstorm не заигнорил по невнимательности, но всё же 😉
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Хм, видел проекты, где это коммитят. Думал, специально, чтобы были одинаковые настройки для работы с проектом
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
.idea папку как правило заносят в .gitignore , однако сами JetBrain рекомендуют некоторые файлы (отвечающие за внешние папки, конфигурацию дебага и информацию о cvs) коммитить
| ENV SYMFONY_VERSION ${SYMFONY_VERSION} | ||
|
|
||
| # Download the Symfony skeleton and leverage Docker cache layers | ||
| RUN composer create-project "${SKELETON} ${SYMFONY_VERSION}" . --stability=$STABILITY --prefer-dist --no-dev --no-progress --no-interaction; \ |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Создание приложения в Dockerfile - не лучшее тому место. Наш контейнер должен быть наиболее атомарен. Например, содержать сугубо php-fpm в себе. Как и рекомендуется самим докером - у одного контейнера - одна зона ответственности.
К тому же с каждой директивой dockerfile становится тяжеловеснее, приобретая по слою за команду. Что тоже не очень сказывается на его размере и скорости разворачивания.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Я брал уже готовый (вроде даже из доки симфони) и немного изменял.
Как это лучше реализовать?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Это действительно изменённый Dockerfile из рекомендаций symfony по работе с докером . Это нормальное решение, однако, как заметил @DaringFrogling, не атомарное, потому что код не отделён от инфраструктуры. Идеологически правильно было бы код хранить в отдельном контейнере и шарить его volume между контейнерами, но при этом, увеличивается сетевая нагрузка внутри докера, что будет добавлять неудобства при разработке (особенно касается macos)
| // this down() migration is auto-generated, please modify it to your needs | ||
| $this->addSql('CREATE SCHEMA public'); | ||
| $this->addSql('DROP SEQUENCE option_id_seq CASCADE'); | ||
| $this->addSql('CREATE SEQUENCE link_keyword_seq INCREMENT BY 1 MINVALUE 1 START 1'); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Не понимаю почему в down() создаётся сиквенс и схема.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Автогенерация, не знал, что за ней нужно следить 😓
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Не понимаю почему в down() создаётся сиквенс и схема.
Потому что в up она дропается. Значит, были изменения в Entity
|
|
||
| protected function configure(): void | ||
| { | ||
| $this->setHelp('This command insert needed records to options table in db'); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Фикстуры лучше наполнять с помощью DoctrineFixturesBundle. https://symfony.com/bundles/DoctrineFixturesBundle/current/index.html
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
То есть инициализировать конфиг в бд при помощи фикстур? Но это так же делать в команде?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Если данные обязательно должны быть то допустимо инициализировать их в миграции. Более правильный вариант - фикстуры (с ними нужно быть осторожным - неверный вызов перезатрёт имеющиеся в бд данные). Данный вариант с вызовом команды тоже возможен, но он менее очевидный (его нужно прописывать в документации к деплою). Так же допустим и вариант создания next_id во время выполнения кода, без необходимости запуска каких либо команд
| use Symfony\Component\HttpFoundation\JsonResponse; | ||
| use Throwable; | ||
|
|
||
| class JsonErrorController |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Наверное это лучше вынести в слушатель ExceptionEvent'a. https://github.com/symfony/symfony/blob/6.0/src/Symfony/Component/HttpKernel/Event/ExceptionEvent.php
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Искал пример с подобной реализацией (видимо плохо искал)... Спасибо.
Есть у такой реализации ещё профиты, кроме удобства и простоты?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Получается, любое внутреннее исключение отобразится пользователю, что не очень хорошо. Тот метод, который предложил @DaringFrogling c обработкой ошибок более гибкий, но практически равнозначен текущей реализации. Здесь же стоит добавить логирование ошибки и отображение пользователю общей информации о ней
| */ | ||
| public function create(Request $request, ManagerRegistry $doctrine, UrlTitleService $titleService): Response | ||
| { | ||
| // code in style of fat controller, because of no time... Need to do validations, some service etc |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Если нет времени, то проще сразу писать всё в сервисах же 😂
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Согласен, вся бизнес логика и работа с БД должны быть выделены в отдельный слой
| { | ||
| // code in style of fat controller, because of no time... Need to do validations, some service etc | ||
| if (empty($request->get('long_url'))) { | ||
| throw new BadRequestHttpException('long_url is required'); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Нецелесообразная проверка. Её надо передать в валидатор и он выдат validationexception.
| } | ||
|
|
||
| $link = new Link(); | ||
| $link->setUrl($request->get('long_url')); //todo: need to validate url |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Тонкий контроллер содержит минимум логики в себе. А именно просто подготовливает реквест данные для передачи в сервисный слой: валидирует, создаёт команду/квери/дто объекты, вызывает сервисный слой и отдаёт ответ.
https://martinfowler.com/eaaCatalog/serviceLayer.html
https://habr.com/ru/post/547510/
Пример реализации можно подсмотреть у меня
https://github.com/DaringFrogling/url-shorter/pull/1/files#diff-d39b69a8e9b69fba23cbb7dc9c08302e4a2418d6e7ffe2b2cc94895ac05f95f4 контроллер
https://github.com/DaringFrogling/url-shorter/pull/1/files#diff-abc82886f1d6f24ae786da382083a02d30cc178212dafa29a35886f10b3f7465 сервис
| $link->setTags($request->get('tags', [])); | ||
| } | ||
|
|
||
| $entityManager = $doctrine->getManager(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
За выборку данных отвечают репозитории. И только.
|
|
||
| $entityManager->flush(); | ||
|
|
||
| $result = $doctrine->getRepository(Link::class)->findOneBy(['url' => $link->getUrl()]); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
зачем это выполнять, если у тебя объект ссылки уже есть выше? Почему нельзя вернуть его? Он же даже с id уже будет
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Перенял привычку с проекта, где нет менеджера...
| public function update(ManagerRegistry $doctrine, string $id): Response | ||
| { | ||
| $entityManager = $doctrine->getManager(); | ||
| $link = $entityManager->getRepository(Link::class)->find($id); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
для простого find() можно же сделать $entityManager->find(Link::class, $id);, а для кейса "по id из запроса грузим сущность" можно вообще в сигнатуре метода это указать и у тебя симфони автоматом все подгрузит:
public function update(?Link $link, ManagerRegistry $doctrine, string $id): Response
{
if (!$link) {
throw $this->createNotFoundException(
'No product found for id '.$id
);
}
}если же у параметра убрать nullable, то симфони сам будет обрабатывать случаи, когда сущность не найдена
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
😮
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Это поведение где-нибудь настраивается?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
не понял что должно "настраиваться", я говорил про https://symfony.com/doc/current/routing.html#parameter-conversion (и там ссылки есть где подробнее почитать можно)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Это поведение где-нибудь настраивается?
Такое поведение доступно благодаря SensioFrameworkExtraBundle для типов данных \DateTime и для Entity доктрины. Подробнее - в документации к конверторам
| if (!$link) { | ||
| throw $this->createNotFoundException( | ||
| 'No product found for id '.$id | ||
| ); | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
не уверен, что в этом случае стоит кидать ошибку. Мы же хотим удалить сущность, если ее нет - она удалена, т.е. все выполнено уже. В большинстве случаев здесь стоит вернуть success
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Насколько я видел, либо ошибка, либо no content отдают. Например, https://stripe.com/docs/api/invoices/delete
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Это как раз вопрос про идемпотентность операции delete. Несмотря на разный код ответа, сам результат ответа должен будет одинаковый: записи ведь уже нет
| * @ORM\Id | ||
| * @ORM\GeneratedValue(strategy="CUSTOM") | ||
| * @ORM\CustomIdGenerator(class=ShuffleKeywordGenerator::class) | ||
| * @ORM\Column(type="string", length=200, unique=true) | ||
| */ | ||
| private $keyword; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
id по строке работает не очень, потом проблемы со скоростью можно наблюдать. Лучше uuid, либо автоинкремент
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
А есть, где почитать про проблемы со скоростью? Есть проект с такой логикой, там щас 12млн записей и просадок нет (может быть пока?)
Задумка была в том, чтобы сгенерированное слово и было уникальным ключом. Смысл в id, если оно не будет использоваться нигде.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
я вот такую книжку читал: https://www.oreilly.com/library/view/high-performance-mysql/9780596101718/, но она старая уже, вроде вот это новое издание: https://mobile.twitter.com/tobias_petry/status/1472951375646625799?s=21
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
ну и раз "чтобы сгенерированное слово и было уникальным ключом", то сразу косяк: а если мы захотим поменять сокращатель? Либо старый себя изжил, либо хотим разные алгоритмы попробовать?
Если два разных логических значения задействуют одно и то же свойство, то однажды станет больно: надо будет разделить, а как понять, что в одном месте это id, а в другом короткая ссылка?
Если все же хочется все завязать на одно поле, то надо хотя бы разными методами выделить разное смысловое значение. Тогда если понадобится убрать с id логику минимизации урл, то можно будет легко поменять реализацию
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Технически для данной задачи всё ок, можно использовать shortUrl в качестве primary key и на скорость работы это никак не повлияет. Если же ожидается изменение структуры таблицы, бд или метода генерации ссылок, то проблемы, о которых пишет @BOPOH возникнут и их придётся решать.
| if (!$result) { | ||
| $nextId = new Option(); | ||
| $nextId->setName('next_id'); | ||
| $nextId->setValue('1'); | ||
|
|
||
| $manager->persist($nextId); | ||
| $manager->flush(); | ||
| $output->writeln('Done!'); | ||
|
|
||
| return Command::SUCCESS; | ||
| } else { | ||
| $output->writeln('Option next_id already exists'); | ||
|
|
||
| return Command::FAILURE; | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
ветка else в данном случае не нужна: если вошло в условие, то никогда из него уже не выйдет (потому что return)
было бы нагляднее переписать в следующем виде:
if ($result) {
$output->writeln('Option next_id already exists');
return Command::FAILURE;
}
$nextId = new Option();
...
return Command::SUCCESS;| use Doctrine\ORM\EntityManager; | ||
| use Doctrine\ORM\Id\AbstractIdGenerator; | ||
|
|
||
| class SequentialKeywordGenerator extends AbstractIdGenerator |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
очень похоже на то, что здесь ты пытался создать собственный автоинкремент на строках. А зачем переусложнять? Подозреваю, что из-за этого у тебя и падает всё.
стоило задействовать дефолтные реализации (автоинкремент на (биг)интах), а когда этого стало бы не хватать - тогда уже придумывать кастомные реализации
+ кучу времени потерял на реализацию, вот на сервисы времени и не хватило
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Да, почти так и было, только это для строки с фиксированной длиной. И он не используется...
| use Symfony\Component\DomCrawler\Crawler; | ||
| use Symfony\Contracts\HttpClient\HttpClientInterface; | ||
|
|
||
| class UrlTitleService |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
D из SOLID советует работать с абстракциями. В некоторых случаях, если вероятность изменения реализации очень мала, то можно поступить так, как реализовано:
- создаем класс
- везде его задействуем
и если появилась необходимость изменения реализации, то создаем интерфейс с именем текущего класса, а имя класса меняем на указание реализации (ну и интерфейс созданный указываем)
отсюда вытекает следующая мысль: имя класса должно быть абстрактным, без указания реализации: TitleService (ну и лучше указать для чего Title - например, LinkTitleService)
но в данном случае смена реализации более чем возможна, тем более имя класса само говорит об этом: url title, а значит имело смысл сразу выделить интерфейс и везде использовать его, а не конкретный класс
если же под url title имелось ввиду link title из задачи, то стоило так и назвать, а не плодить в одном контексте разные названия для одной и той же сущности
lis-dev
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
В целом следующие замечания:
- Конфигурация: она дефолтная, но очень избыточная (в докерфайле лишние библиотеки, веб-сервер, mercure и т.п.).
- Переусложнённая логика с Options и всё, что связано с этой Entity
- Использование ManagerRegistry для получения и репозитория и EntityManager'a - лучше разделять их использование и внедрять как зависимость только то, что нужно и когда нужно
- Кодстайл: php-cs-fixer поможет придерживаться общего стиля
| @@ -0,0 +1 @@ | |||
| github: [ dunglas ] | |||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Это, видимо, случайно попало в коммит
| ENV SYMFONY_VERSION ${SYMFONY_VERSION} | ||
|
|
||
| # Download the Symfony skeleton and leverage Docker cache layers | ||
| RUN composer create-project "${SKELETON} ${SYMFONY_VERSION}" . --stability=$STABILITY --prefer-dist --no-dev --no-progress --no-interaction; \ |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Это действительно изменённый Dockerfile из рекомендаций symfony по работе с докером . Это нормальное решение, однако, как заметил @DaringFrogling, не атомарное, потому что код не отделён от инфраструктуры. Идеологически правильно было бы код хранить в отдельном контейнере и шарить его volume между контейнерами, но при этом, увеличивается сетевая нагрузка внутри докера, что будет добавлять неудобства при разработке (особенно касается macos)
| --with github.com/dunglas/mercure \ | ||
| --with github.com/dunglas/mercure/caddy \ | ||
| --with github.com/dunglas/vulcain \ | ||
| --with github.com/dunglas/vulcain/caddy |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
mercure и vulcain , насколько я вижу, в приложении не используются
| use Symfony\Component\HttpFoundation\Response; | ||
| use Symfony\Component\HttpKernel\Event\RequestEvent; | ||
|
|
||
| final class RequestTransformerListener |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Т.к. здесь меняется тело реквеста при каждом запросе и используется sensio framework extra bundle, я бы рекомендовал использовать ParamConverter
| - '../src/Kernel.php' | ||
| - '../src/Tests/' | ||
|
|
||
| App\EventListener\RequestTransformerListener: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Несмотря на некоторые преимущества EventListeners, я бы всё же рекомендовал использовать EventSubscribers. Они немного нагляднее и autowire поможет не прописывать каждый из них в конфигурации.
| { | ||
| $nextId = $this->findOneBy(['name' => 'next_id']); | ||
| if (!$nextId) { | ||
| throw new NextIdNotFound(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Лучше не создавать под каждый кейс свой эксепшн. И стараться оперировать более общими типа "нот фунд эксепшн хттп". Ибо когда проект разрастётся, получится уйма непонятных слишком конкретных эксепшнов.
Частично соглашусь. NextIdNotFound - определяет конкретную ситуацию, а не класс возможных ситуаций. Было бы уместнее сделать, например, RequiredOptionNotFoundExcpeption('next_id not found'); .
| LinkRepository $linkRepository, | ||
| string $charset, | ||
| int $keywordLength | ||
| ) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
В php начиная с версии 8.0 можно делать constructor property promotion:
public function __construct(
private OptionRepository $optionRepository,
private LinkRepository $linkRepository,
private string $charset,
private int $keywordLength
) {}|
|
||
| } | ||
|
|
||
| public function generate(EntityManager $em, $entity) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
метод не использует ни $em, ни $entity
| $charsetLen = strlen($this->charset); | ||
| while ($strLen < $this->keywordLength) { | ||
| while ($number >= $charsetLen) { | ||
| $mod = bcmod($number, (string)$charsetLen); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
из-за declare(strict_types=1); здесь php должен ругаться, ведь bcmod принимает в параметрах string
|
|
||
| use Symfony\Component\HttpKernel\Exception\HttpException; | ||
|
|
||
| class NextIdNotFound extends HttpException |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Наследование от HttpException неудачное, т.к. класс ошибки никак не связан с http.
Первый мой проект на Symfony и решил сразу на 6 с php 8.1. Соответственно без api platform.
Идея была в том, чтобы сделать генерацию keyword для короткой ссылки на уровне id в сущности.
Потратил много времени на документацию + разные неожиданные ошибки.
В данный момент при попытке что нибудь сделать валится в фаталити. Я так предполагаю, что накосячил с аннотациями или нужно запускать в режиме no-debug
`Fatal error: Allowed memory size of 536870912 bytes exhausted (tried to allocate 20480 bytes) in /srv/app/vendor/doctrine/orm/lib/Doctrine/ORM/Mapping/Driver/AnnotationDriver.php on line 728
Fatal error: Allowed memory size of 536870912 bytes exhausted (tried to allocate 65536 bytes) in /srv/app/vendor/symfony/error-handler/DebugClassLoader.php on line 287`
КОРОТКО: На сервисы разделить не успел, валидацию не реализовал, действия по переходу по ссылке тоже... Не хватило времени и знаний по фреймворку.
Хотелось бы узнать как можно разделить на сервисы логику в этом задании, как правильно реализовать валидацию