• Symfony - Как изменить форму Бандла?

    BoShurik
    @BoShurik Куратор тега Symfony
    Symfony developer
    Можно использовать Form Type Extension

    use Symfony\Component\Form\AbstractTypeExtension;
    use Symfony\Component\Form\FormBuilderInterface;
    
    class BundleFormTypeExtension extends AbstractTypeExtension
    {
        public static function getExtendedTypes(): iterable
        {
            return [BundleFormType::class];
        }
    
        public function buildForm(FormBuilderInterface $builder, array $options)
        {
            $builder->remove('someField');
        }
    }
    Ответ написан
    Комментировать
  • Самодиагностика CRM системы?

    BoShurik
    @BoShurik Куратор тега Symfony
    Symfony developer
    Делал нечто подобное, получилось так:
    namespace App\Doctor;
    
    use App\Doctor\Check\CheckInterface;
    
    final class Doctor
    {
        /**
         * @var CheckInterface[]
         */
        private iterable $checks;
    
        public function __construct(iterable $checks)
        {
            $this->checks = $checks;
        }
    
        /**
         * @return Violation[]|array
         */
        public function check(): array
        {
            $violations = [];
            foreach ($this->checks as $check) {
                $violations[$check->feature()] = array_merge($violations[$check->feature()] ?? [], $check->violations());
            }
    
            return $violations;
        }
    }

    namespace App\Doctor\Check;
    
    use App\Doctor\Violation;
    
    interface CheckInterface
    {
        public function feature(): string;
    
        /**
         * @return Violation[]
         */
        public function violations(): array;
    }

    services:
        _instanceof:
            App\Doctor\Check\CheckInterface:
                tags:
                    - { name: app.doctor.check }
    
        App\Doctor\Doctor:
            arguments:
                $checks: !tagged app.doctor.check

    public function doctorAction(): JsonResponse
    {
        return $this->json($this->doctor->check());
    }

    {
        "foo": [], // Ok
        "bar": [
            { "message": "Отсутствуют статусы", "treatment": "Добавьте статусы" }
        ]
    }


    Как вариант, можно группировать проверки по фичам, чтоб не проверять все, когда нужна информация по конкретной фиче, но для этого надо будет CompilerPass заюзать
    Ответ написан
    3 комментария
  • Как сообщить EntityManager о сущности?

    BoShurik
    @BoShurik Куратор тега Symfony
    Symfony developer
    https://symfony.com/doc/current/components/seriali...

    Это можно обернуть в какой-нибудь кастомный нормалайзер
    use Doctrine\ORM\EntityManagerInterface;
    use Symfony\Component\Serializer\Normalizer\AbstractObjectNormalizer;
    use Symfony\Component\Serializer\Normalizer\DenormalizerInterface;
    use Symfony\Component\Serializer\Normalizer\ObjectNormalizer;
    
    class MyObjectDenormalizer implements DenormalizerInterface
    {
        private ObjectNormalizer $objectNormalizer;
        private EntityManagerInterface $entityManager;
    
        public function __construct(ObjectNormalizer $objectNormalizer, EntityManagerInterface $entityManager)
        {
            $this->objectNormalizer = $objectNormalizer;
            $this->entityManager = $entityManager;
        }
    
        public function denormalize($data, string $type, string $format = null, array $context = [])
        {
            if ($id = $data['id'] ?? null) {
                $object = $this->entityManager->getRepository($type)->find($id);
                $context = [
                    AbstractObjectNormalizer::OBJECT_TO_POPULATE => $object,
                ];
                unset($data['id']);
            }
    
            return $this->objectNormalizer->denormalize($data, $type, $format, $context);
        }
    
        public function supportsDenormalization($data, string $type, string $format = null)
        {
            return $this->objectNormalizer->supportsDenormalization($data, $type, $format);
        }
    }


    Но лучше подставлять объект в контроллере на основании данных из роута (/comment/{id}/edit), т.к. есть возможность подменить id и отредактировать другую сущность (к которой, к примеру, у пользователя доступа нет)
    Ответ написан
    1 комментарий
  • Можно ли в symfony проверить есть ли доступ у определённой роли к url адресу?

    BoShurik
    @BoShurik Куратор тега Symfony
    Symfony developer
    Проще зайти с другой стороны.
    - при выходе помечаем этот факт в сессии
    - если после редиректа он ловит access-denied и присутствует флаг, то просто перенаправляем на главную
    - убираем флаг, как только пользователь приземлился на страницу с 2XX кодом, чтоб в дальнейшем его перенапаряляло на страницу логина

    use Symfony\Component\EventDispatcher\EventSubscriberInterface;
    use Symfony\Component\HttpFoundation\RedirectResponse;
    use Symfony\Component\HttpKernel\Event\ExceptionEvent;
    use Symfony\Component\HttpKernel\Event\ResponseEvent;
    use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
    use Symfony\Component\Security\Core\Exception\AccessDeniedException;
    use Symfony\Component\Security\Http\Event\LogoutEvent;
    
    class LogoutRedirectSubscriber implements EventSubscriberInterface
    {
        private const KEY = 'logout';
    
        private UrlGeneratorInterface $urlGgenerator;
    
        public static function getSubscribedEvents()
        {
            return [
                ExceptionEvent::class => ['onException', 2], // Before \Symfony\Component\Security\Http\Firewall\ExceptionListener
                ResponseEvent::class => 'onResponse',
                LogoutEvent::class => 'onLogout',
            ];
        }
    
        public function __construct(UrlGeneratorInterface $urlGgenerator)
        {
            $this->urlGgenerator = $urlGgenerator;
        }
    
        public function onException(ExceptionEvent $event): void
        {
            if (!$event->isMasterRequest()) {
                return;
            }
            $exception = $event->getThrowable();
            if (!$exception instanceof AccessDeniedException) {
                return;
            }
            $session = $event->getRequest()->getSession();
            if ($session->has(self::KEY)) {
                $event->setResponse(new RedirectResponse($this->urlGgenerator->generate('index')));
                $event->stopPropagation();
            }
        }
    
        public function onResponse(ResponseEvent $event): void
        {
            if (!$event->isMasterRequest()) {
                return;
            }
            if ($event->getResponse()->getStatusCode() >= 300) {
                return;
            }
            $session = $event->getRequest()->getSession();
            if ($session->has(self::KEY)) {
                $session->remove(self::KEY);
            }
        }
    
        public function onLogout(LogoutEvent $event): void
        {
            $event->getRequest()->getSession()->set(self::KEY, true);
        }
    }


    На вопрос "Можно ли в symfony проверить есть ли доступ у определённой роли к url адресу?" - ответ нет, разве что вы добавите всем роутам метаданные в options и будете проверять их в обработчике LogoutEvent
    Ответ написан
  • Как упростить функцию?

    BoShurik
    @BoShurik
    Symfony developer
    function json_tree_tags_recursive($data, &$result, $path = '')
    {
        foreach ($data as $key => $value) {
            if ($path !== '') {
                $result[$path . $key] = $value;
            } elseif (!is_array($value)) {
                $result[$key] = $value;
            }
            if (is_array($value)) {
                json_tree_tags_recursive($value, $result, $path . $key . '_');
            }
        }
    }
    json_tree_tags_recursive($data, $result);
    print_r($result);
    Ответ написан
    1 комментарий
  • В какой момент проверять уникальность?

    BoShurik
    @BoShurik Куратор тега Symfony
    Symfony developer
    Комментировать
  • Своя иерархия папок в Symfony?

    BoShurik
    @BoShurik Куратор тега Symfony
    Symfony developer
    1. https://github.com/symfony/recipes/blob/master/doc...
    Но идея плохая, т.к. миграции - это не код.
    2. Можно при конфигурировании контейнера и роутов в Kernel.php разбирать структуру папок и подключать все динамически там.
    3. Нет. Как создать entity не по стандартному пути?

    FYI, моя структура папок сейчас выглядит так:
    migrations/
    src/
    -- Controller/
    ---- User/
    ---- ModuleName/
    -- Entity/
    ---- User/
    ---- ModuleName/
    -- User/
    --- Dto/
    --- Repository/
    --- Service/
    -- ModuleName/
    --- Dto/
    --- Repository/
    --- Service/

    Во-первых, нет заморочек с конфигурированием, во-вторых, если первый раз включаешься в проект, то идеально сразу видеть набор сущностей и контроллеры, а не бегать по папкам модулей в их поисках, плюс контроллеры часто сложно отнести к какому-то конкретному модулю.
    Ответ написан
    1 комментарий
  • Как сохранить форму, в которую встроена коллекция другой формы?

    BoShurik
    @BoShurik Куратор тега Symfony
    Symfony developer
    По вашей же ссылке предлагается решение:
    // src/AppBundle/Form/CustomerType.php
    public function buildForm(FormBuilderInterface $builder, array $options)
    {
        //....
        $builder
            ->add('phones',CollectionType::class, array(
                'by_reference' => false,
                // ...
            ));
        //....
    }


    class Customers implements UserInterface
    {
        /**
         * @ORM\OneToMany(targetEntity="AppBundle\Entity\Phone", mappedBy="customer_id", cascade={"persist", "remove", "merge"})
         */
        private $phones; // Это же коллекция, нужно множественное число, чтобы работали adder и remover
    
        public function addPhone(Phone $phone)
        {
            $phone->setCustomer($this);
            $this->phones->add($phone);
        }
    
        public function removePhone(Phone $phone)
        {
            $phone->setCustomer(null);
            $this->phones->removeElement($phone);
        }
    }


    // src/AppBundle/Form/PhoneType.php
    // $builder->add('customerId', HiddenType::class); // не нужен
    Ответ написан
    1 комментарий
  • Как добавить первичного администратора в Symfony?

    BoShurik
    @BoShurik Куратор тега Symfony
    Symfony developer
    Если используется FOSUserBundle, то надо использовать команду
    bin/console fos:user:create username em@ai.il 'p@55w0rd'
    .

    В случае своего решения, эта команду надо создать самому. У меня она выглядит как-то так (по сути повторяет код экшена контроллера со своими нюансами)

    CreateCommand

    /**
     * @psalm-suppress PropertyNotSetInConstructor
     */
    class CreateCommand extends AbstractCommand
    {
        private DocumentManager $documentManager;
        private ValidatorInterface $validator;
        private PasswordGenerator $passwordGenerator;
        private AdministratorMapper $mapper;
    
        public function __construct(
            DocumentManager $documentManager,
            ValidatorInterface $validator,
            PasswordGenerator $passwordGenerator,
            AdministratorMapper $mapper
        ) {
            parent::__construct();
    
            $this->documentManager = $documentManager;
            $this->validator = $validator;
            $this->passwordGenerator = $passwordGenerator;
            $this->mapper = $mapper;
        }
    
        /**
         * @psalm-suppress MissingReturnType
         */
        protected function configure()
        {
            $this
                ->setName('app:administrator:create')
                ->addArgument('username', InputArgument::REQUIRED, 'Username')
                ->addOption('password', 'p', InputOption::VALUE_REQUIRED, 'Password')
                ->setDescription('Creates administrator')
            ;
        }
        
        protected function execute(InputInterface $input, OutputInterface $output)
        {
            /** @var string $username */
            $username = $input->getArgument('username');
            /** @var string|null $plainPassword */
            $plainPassword = $input->getOption('password');
            if (!$plainPassword) {
                $plainPassword = $this->passwordGenerator->generate();
            }
    
            $model = new AdministratorModel();
            $model->enabled = true;
            $model->username = $username;
            $model->password = $plainPassword;
    
            $errors = $this->validator->validate($model);
            if (\count($errors) > 0) {
                $this->io->error('Can\'t create administrator');
                $this->printConstraintViolations($errors);
    
                return 1;
            }
    
            $administrator = $this->mapper->map($model);
    
            $this->documentManager->persist($administrator);
            $this->documentManager->flush();
    
            $this->io->writeln(sprintf('Administrator <info>%s</info> with password <info>%s</info> has been created', $administrator->getUsername(), $plainPassword));
    
            return 0;
        }
    }



    bin/console app:administrator:create vasx3

    Создавать пользователей, а тем более администраторов, через миграции и фикстуры - риск, т.к. в случае утечки кода все эти данные будут доступны третьим лицам
    Ответ написан
    4 комментария
  • Как создать entity не по стандартному пути?

    BoShurik
    @BoShurik Куратор тега Symfony
    Symfony developer
    Так нельзя

    Как вариант (если у вас все сущности все равно находятся в неймеспейсе Entity, т.к. он захардкожен) - конфигурировать бандл перед каждой генерацией, а потом возвращать назад
    maker:
        root_namespace: 'App\Common'


    Не забудьте добавить неймспейс в конфиг доктрины до генерации, т.к. иначе вы сгенерируете класс сущности и репозиторий, но добавить поля вам не дадут.
    mappings:
        App:
            is_bundle: false
            type: annotation
            dir: '%kernel.project_dir%/src/Entity'
            prefix: 'App\Entity'
            alias: App
        AppСommon:
            is_bundle: false
            type: annotation
            dir: '%kernel.project_dir%/src/Common/Entity'
            prefix: 'App\Common\Entity'
            alias: AppСommon
    Ответ написан
    2 комментария
  • Как в Symfony работать с вложенными объектами и формами?

    BoShurik
    @BoShurik Куратор тега Symfony
    Symfony developer
    Формы не очень хорошо работают с динамическим контентом, оптимальным решением будет перенести форму на фронтенд, а со стороны бекенда использовать serializer + validator + argument-resolver (см. Как правильно фильтровать и мапить данные при реализации API на Symfony4?)
    Если у вас в specs всегда один и тот же набор объектов и нет динамических параметров, то вложенные формы вполне могут сработать
    Ответ написан
    2 комментария
  • Composer: как добавить приватный репозиторий в зависимости?

    BoShurik
    @BoShurik
    Symfony developer
    repositories в зависимостях игнорируется
    root-only - значит он работает только для основного composer.json
    Ответ написан
    2 комментария
  • Как в symfony secure проверить авторизованность пользователя?

    BoShurik
    @BoShurik Куратор тега Symfony
    Symfony developer
    https://symfony.com/doc/current/components/securit...
    /** @var AuthorizationCheckerInterface $authorizationChecker */
    if (!$authorizationChecker->isGranted('ROLE_USER')) {
        throw new AccessDeniedException();
    }


    Если роль пользователя неважна (хотя хорошая практика все-таки всегда выдавать базовую роль)
    https://symfony.com/doc/current/security.html#chec...
    /** @var AuthorizationCheckerInterface $authorizationChecker */
    if (!$authorizationChecker->isGranted('IS_AUTHENTICATED_REMEMBERED')) {
        throw new AccessDeniedException();
    }
    Ответ написан
    4 комментария
  • Почему composer update выдает ошибку?

    BoShurik
    @BoShurik
    Symfony developer
    {
        "require": {
            "phpmailer/phpmailer": "^6.0",
            "bogdaan/viber-bot-php": "^0.0.12",
            "brickpop/php-rest-curl": "^1.0"
        },
        "repositories": {
            "php-rest-curl": {
                "type": "package",
                "package": {
                    "name": "brickpop/php-rest-curl",
                    "version": "1.0.0",
                    "source": {
                        "url": "git@github.com:brickpop/php-rest-curl.git",
                        "type": "git",
                        "reference": "master"
                    },
                    "autoload": {
                        "files": ["rest.inc.php"]
                    }
                }
            }
        }
    }
    Ответ написан
  • JsonResponse убирает индексы массива из результата, если они начинаются с нуля?

    BoShurik
    @BoShurik Куратор тега Symfony
    Symfony developer
    JsonResponse не при чем. Дело в json_encode. Вам нужна опция JSON_FORCE_OBJECT

    use Symfony\Component\HttpFoundation\JsonResponse;
    
    $data = [
        'List' => [
            0 => [
                'id' => 'id0',
            ],
            1 => [
                'id' => 'id1',
            ],
        ],
    ];
    dump(
        new JsonResponse(
            json_encode($data, JsonResponse::DEFAULT_ENCODING_OPTIONS | JSON_FORCE_OBJECT),
            200,
            [],
            true
        )
    );
    // or
    $response = new JsonResponse($data);
    $response->setEncodingOptions(JsonResponse::DEFAULT_ENCODING_OPTIONS | JSON_FORCE_OBJECT);
    dump($response->getContent());


    Либо, если это нужно регулярно и много где, можно создать свой JsonResponse, который наследуется от стандартного, в котором изменить encodingOptions на нужные
    Ответ написан
    Комментировать
  • Как вы получаете количество оставшихся записей для выборки подмножеств?

    BoShurik
    @BoShurik Куратор тега Symfony
    Symfony developer
    https://github.com/BabDev/Pagerfanta

    use Pagerfanta\Pagerfanta;
    
    class PaginatedCollection
    {
        private int $page;
        private int $size;
        private int $total;
        private int $pages;
        private array $items;
    
        public function __construct(Pagerfanta $pagerfanta)
        {
            $this->page = $pagerfanta->getCurrentPage();
            $this->size = $pagerfanta->getMaxPerPage();
            $this->total = $pagerfanta->getNbResults();
            $this->pages = $pagerfanta->getNbPages();
            $this->items = iterator_to_array($pagerfanta);
        }
    
        public function getPage(): int
        {
            return $this->page;
        }
    
        public function getSize(): int
        {
            return $this->size;
        }
    
        public function getTotal(): int
        {
            return $this->total;
        }
    
        public function getPages(): int
        {
            return $this->pages;
        }
    
        public function getItems(): array
        {
            return $this->items;
        }
    }


    // Repository
    public function findPaginatedBySupplierId(int $supplier_id)
    {
        return new Pagerfanta(DoctrineORMAdapter($this->getSupplierIdQueryBuilder($supplier_id)));
    }
    
    private function getSupplierIdQueryBuilder(int $supplier_id)
    {
        $qb = $this->createQueryBuilder('sj');
        $qb = $qb
            ->select("sj")
            ->orderBy('sj.datetime', 'ASC')
            ->andWhere("sj.supplier = :supplier_id")
            ->setParameter("supplier_id", $supplier_id);
    
        return $qb;
    }


    // Controller
    public function action()
    {
        // ...
        return $this->json($this->paginatedCollection($repository->findPaginatedBySupplierId($id), $request));
    }
    
    // AbstractController
    protected function paginatedCollection(Pagerfanta $pagination, Request $request, int $size = 20): PaginatedCollection
    {
        $pagination = $this->paginate($pagination, $request, $size);
    
        return new PaginatedCollection($pagination);
    }
    
    protected function paginate(Pagerfanta $pagination, Request $request, int $size = 20): Pagerfanta
    {
        $pagination->setMaxPerPage($size);
        $pagination->setCurrentPage($request->query->getInt('page', 1));
    
        return $pagination;
    }


    {
        "page": 1,
        "size": 20,
        "total": 76,
        "pages": 4,
        "items": [...]
    }
    Ответ написан
  • Как десериализовать массив json-ов c отношением OneToMany?

    BoShurik
    @BoShurik Куратор тега Symfony
    Symfony developer
    Symfony serializer как десериализовать массив с объектами?

    Плюс, вам надо прописать типы в phpDoc
    /**
     * @var \App\Entity\IpContract[]
     *
     * @ORM\OneToMany(targetEntity="App\Entity\IpContract", mappedBy="users", orphanRemoval=true)
     */
    private $ipContracts;
    Ответ написан
  • Избавиться от __initializer__, __cloner__, __isInitialized__ при выводе json?

    BoShurik
    @BoShurik Куратор тега Symfony
    Symfony developer
    1. Создать свой normalizer
    2. Игнорировать эти атрибуты
    return $this->json($user, 200, [], [
        AbstractNormalizer::IGNORED_ATTRIBUTES => ['__initializer__', '__cloner__', '__isInitialized__'],
    ]);

    3. Пробросить это глобально в ObjectNormalizer
    // App\Kernel
    protected function configureContainer(ContainerBuilder $container, LoaderInterface $loader)
    {
        // ...
        $container->addCompilerPass(new class implements CompilerPassInterface {
            public function process(ContainerBuilder $container)
            {
                $container->getDefinition('serializer.normalizer.object')->setArgument(6, [
                    AbstractNormalizer::IGNORED_ATTRIBUTES => ['__initializer__', '__cloner__', '__isInitialized__'],
                ]);
            }
        });
    }
    Ответ написан
    Комментировать
  • Как абстрагировать аргумент конструктора при таком подходе?

    BoShurik
    @BoShurik
    Symfony developer
    Если я правильно понял, то
    abstract class AbstractAdapter extends \CloudCreativity\LaravelJsonApi\Eloquent\AbstractAdapter
    {
        public function __construct(CursorStrategy $paging)
        {
            parent::__construct($this->getModelInstance(), $paging);
        }
    
        abstract protected function getModelInstance();
    
        // ...
    }
    Ответ написан
    Комментировать
  • Как изменить данные в блоке импортированного шаблона родителя?

    BoShurik
    @BoShurik
    Symfony developer
    В том варианте, который вы описали, это невозможно.
    Вам придется, во-первых, вывести все ваши инклюды (ну или только те, где нужна возможность изменения блоков) в отдельные блоки, во-вторых, придется вместо include использовать embed тег.
    https://twigfiddle.com/rxgc61
    Ответ написан
    1 комментарий