• Как лучше описывать документацию API в PHP 8.1 через аннотации или атрибуты?

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

    @Sellisent
    <?php
    
    require_once 'vendor/autoload.php';
    
    use FFI\TDLib\TDLib;
    
    // Создаем экземпляр класса FFITdLib
    $tdlib = new TDLib();
    
    // Устанавливаем параметры для авторизации
    $api_id = 'YOUR_API_ID';
    $api_hash = 'YOUR_API_HASH';
    $phone_number = 'YOUR_PHONE_NUMBER';
    
    // Выполняем авторизацию
    $tdlib->send([
        '@type' => 'setTdlibParameters',
        'parameters' => [
            'api_id' => $api_id,
            'api_hash' => $api_hash,
            'use_test_dc' => false,
            'database_directory' => 'tdlib-db',
            'files_directory' => 'tdlib-files',
            'use_file_database' => false,
            'use_chat_info_database' => false,
            'use_message_database' => false,
            'use_secret_chats' => false,
            'api_key' => '',
            'system_language_code' => 'en',
            'device_model' => 'unknown',
            'system_version' => 'unknown',
            'application_version' => '1.0',
            'enable_storage_optimizer' => true,
            'ignore_file_names' => false,
        ],
    ]);
    
    $tdlib->send([
        '@type' => 'checkDatabaseEncryptionKey',
        'key' => '',
    ]);
    
    $tdlib->send([
        '@type' => 'setAuthenticationPhoneNumber',
        'phone_number' => $phone_number,
    ]);
    
    // Получаем код авторизации
    $code = readline('Enter the code: ');
    
    $tdlib->send([
        '@type' => 'checkAuthenticationCode',
        'code' => $code,
    ]);
    
    // Выполняем запрос getChatStatistics
    $chat_id = 'CHAT_ID';
    $result = $tdlib->send([
        '@type' => 'getChatStatistics',
        'chat_id' => $chat_id,
        'is_dark' => false,
    ]);
    
    print_r($result);
    Ответ написан
    1 комментарий
  • Как глобально сохранить куки через компонент HttpFoundation?

    delphinpro
    @delphinpro Куратор тега PHP
    frontend developer
    php отправляет заголовок браузеру что нужно установить куку, а не ставит ее. Бесполезно проверять ее наличие сразу после отправки заголовка. В следующем запросе эта кука вернется браузером на сервер.
    Ответ написан
    Комментировать
  • Почему в php нельзя уточнить класс аргумента при имплементации интерфейса?

    LSP нарушается - интерфейс требует чтобы реализация в качестве аргумента могла принимать любую реализацию ArgInterface, а вы пытаетесь ограничится только одной.
    Ответ написан
    Комментировать
  • Почему в php нельзя уточнить класс аргумента при имплементации интерфейса?

    vabka
    @vabka
    Токсичный шарпист
    Потому что это бы нарушало принцип подстановки Барбары Лисков.

    Интерфейс предполагает, что ты можешь принимать любой объект, который реализует интерфейс ArgInterface.
    => Если ты сузишь тип, то ты уже по факту не сможешь поддерживать контракт.

    По тому при реализации интерфейса можно только расширять тип принимаемых аргументов, но не сужать.
    Ответ написан
    Комментировать
  • Почему когда подписываюсь на Doctrine.Events::preUpdate, он выполняется бесконечно?

    Frostealth
    @Frostealth
    Backend Developer
    Вызов flush() в процессе обработки preUpdate приводит к бесконечной рекурсии, ибо обработчик события вызывается в процессе выполнения предыдущего вызова flush().
    Цепочка вложенных вызовов в вашем случае выглядит следующим образом:

    - EntityManager::flush()
    - UnitOfWork::commit()
    - UnitOfWork::executeUpdates()
    - ListenersInvoker::invoke()
    - OrderEventSubscriber::preUpdate() - вызывается еще до обновления Order и его удаления из UnitOfWork
    - $this->notificationsCreator->createChangeStatusNotification() - создает рекурсию вызовом `flush()`
    - EntityManager::flush() - снова пытается обновить данные Order и вызывает обработку событий `preUpdate`
    - ...
    - EntityManager::flush()
    - ...

    Вызов методов persist(), remove() и т.п. в процессе обработки событий доктрины может привести к неожиданным результатам. Данные просто могут быть не сохранены, как минимум. Крайне не рекомендую менять какие-либо данные в Entity, EM/UoW при обработке событий доктрины.

    В вашем случае можно реализовать свое событие, например OrderStatusChanged. Но лучше использовать более конкретные события вроде OrderCompleted и OrderCanceled.
    И воспользоваться symfony/event-dispatcher.
    Либо создавать уведомление непосредственно там, где выполняете изменение статуса заказа (контроллер, сервис и т.п., зависит от вашей архитектуры) и вызывается flush().
    пример верного вызова

    $this->em->flush();
    $this->events->dispatch(new OrderCompleted($order->id));
    
    // или
    $this->em->flush();
    $this->notificationCreator->createChangeStatusNotification($order->getCustomer());



    P.S. Рекомендую использовать Guard Clauses для уменьшения вложенности и улучшения чтения кода.
    пример

    public function preUpdate(PreUpdateEventArgs $args)
    {
        $entity = $args->getObject();
        if (!$entity instanceof Order) {
        	return;
        }
    
        $this->logger->info($entity);
        $onlyStatusChanged = count($args->getEntityChangeSet()) === 1 && $args->hasChangedField('status');
        if ($onlyStatusChanged) {
            $this->notificationsCreator->createChangeStatusNotification($entity->getCustomer());
        }
        // много кода...
    }

    Ответ написан
    1 комментарий
  • Сделать Entity по имеющейся уже таблице?

    @tukreb
    Это делает депрекейтет команда:
    php bin/console doctrine:mapping:import "App\EntityFromDB" annotation --path=src/EntityFromDB

    https://symfony.com/doc/current/doctrine/reverse_e...

    Почему депрекейтет? Да, потому что она не может покрыть все случае, а поддерживать никто её не хочет.
    Даже если вы создадите по ней сущности, вам придётся всё равно перепахать их все, особенно если у вас всё на атрибутах, т.к она поддерживает только аннотации.

    Запускать миграции нужно всегда. Все изменения БД в современном мире идут ТОЛЬКО через миграции.

    П.С Почему люди предпочитаю сначала код, потом бд, можете почитать здесь :) https://elisdn.ru/blog/104/domain-entities-modelling
    Ответ написан
    4 комментария
  • Symfony Eventlistener идет зацикливание?

    @Flying
    Вы вызываете EntityManager::flush() внутри обработчика события, который сам вызывается в процессе работы
    EntityManager::flush() (а точнее UnitOfWork::commit()), разумеется вы получаете бесконечный цикл.

    Корректным подходом будет реализация, которая накапливает информацию об изменениях и передаёт их для дальнейшего сохранения. Это можно сделать с помощью Doctrine event listener'а, который содержит в себе обработчики следующих событий:
    1. preFlush
    2. postPersist, postUpdate, postRemove
    3. postFlush


    Также, хотя механизмы сохранения изменений могут быть разными, с точки зрения уменьшения времени обработки запроса, логично отложить сохранение изменений "на потом", воспользовавшись поддержкой асинхронных очередей в Symfony Messenger.

    Таким образом ваш обработчик должен действовать примерно следующим образом:
    1. На событие preFlush вы очищаете внутреннюю переменную (например private array $changes = []), в которой будет
      накапливаться информация об изменениях
    2. Обрабатывая события postPersist, postUpdate, postRemove вы сохраняете в $changes
      информацию об изменениях, которую хотите сохранить
    3. В обработчике postFlush вы передаёте накопленные изменения, формируя message, для которого настроен роутинг в async транспорт. Обработчик этого message сможет спокойно сохранить все накопленные изменения, не мешая runtime коду.

    Ответ написан
    2 комментария
  • Как отрефакторить классы с неоднородными конструкторами?

    @Flying
    Я бы использовал service locator для формирования списка сервисов и передавал его в конструктор фабрики.

    Альтернативно можно определить фабрику как service subscriber, тогда формирование service locator'а будет, возможно, ещё проще.

    Если же каким-то образом (например через общий интерфейс или статический метод класса-фабрики), определить список сервисов, которые умеет создавать фабрика, то можно создать compiler pass, который будет формировать service locator на этапе компиляции контейнера через анализ их зависимостей. Это чуть сложнее, но не потребует править код при расширении списка создаваемых фабрикой классов.
    Ответ написан
    Комментировать
  • Где изучать symfony?

    @tukreb
    Я буду более конкретен
    https://symfony.com/doc/current/the-fast-track/ru/...
    Все буквально на блюдечке подано. Даже про докер и git вскользь есть, просто бери и делай проект.
    Ответ написан
    Комментировать
  • Как ограничить очередь на выполнение только 200 задач в час?

    @Flying
    В целом Максим дело говорит, но ситуации бывают разными, поэтому есть вероятность что вам действительно нужно уметь влезать в эти лимиты.

    На самом деле, поскольку вы используете Messenger - то вы уже весьма близки к цели, поскольку Messenger перезапускает обработку сообщения в случае неполной обработки. Просто настройте retry_strategy под свои нужды и оно будет работать. Надо только мониторить failed messages.

    Также, если вы можете позволить себе использовать Symfony 5.2 - то в этой версии появился отдельный компонент RateLimiter, который специально предназначен для решения именно таких задач. Сделайте Messenger middleware для того чтобы помечать сообщения на отправку только если это разрешает RateLimiter, а для остальных бросайте RecoverableMessageHandlingException чтобы отменить обработку. Но, опять же, не забудьте про настройки retry strategy чтобы это не приводило к потерянным сообщениям.
    Ответ написан
    5 комментариев
  • Что может вызывать ошибку 502?

    @Flying
    HTTP 502 - это Bad Gateway. Как видно из описания - это означает что proxy (в вашем случае, предположу, nginx) не может получить ответа от upstream'а (php-fpm в вашем случае). Приведённый вами текст ошибки это прямо подтверждает.

    Из этого можно сделать первый вывод: куда-то пропадают процессы php-fpm. Поскольку рабочие процессы php-fpm даже при падении перезапускает основной процесс php-fpm - навскидку можно сделать несколько первичных предположений:
    1. У вас просто не хватает рабочих процессов php-fpm для обслуживания имеющихся запросов. Проверяйте логи php-fpm и подстраивайте параметры pm, pm.max_children, pm.min_spare_servers
    2. Что-то роняет сами процессы php-fpm. Смотрите логи самого PHP и php-fpm на предмет записей об ошибках, устраняйте их

    Также просто понаблюдайте что происходит с процессами php-fpm на вашем сервере, возможно это даст подсказки.

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

    @Flying
    Конечно контроллер - не место для подобных действий.

    Как правильно указал Владимир Коротенко - перенос кода, отвечающего за работу с внешним API в отдельный сервис - это первое что вам нужно сделать. Это действие открывает для вас массу возможностей: подмена сервиса для разных сценариев, использование заглушек для тестов, вызовы вне контроллеров и т.д. Кроме того сам код при этом становится более простым и поддерживаемым, а также лучше соответствует принципам SOLID.

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

    Для реализации самих запросов внутри сервиса логично использовать Symfony HTTP Client.

    После этого у вас возможны различные сценарии в зависимости от того что же этот API делает.

    Если вы забираете из API данные для подготовки своего ответа и эти данные меняются часто и должны быть всегда актуальны (к примеру какое-нибудь значение котировки акции или курса валюты) - вы вызываете ваш сервис напрямую, а он делает запрос и возвращает ответ.

    Если вы забираете из API данные для подготовки своего ответа, но сами эти данные меняются не постоянно - логично будет добавить в ваш сервис поддержку кэширования с использованием Symfony Cache.

    Если вы забираете данные из API или отправляете данные туда, но они не зависят от контекста текущего запроса - этот процесс логичнее всего вынести в консольную команду, реализованную через Symfony Console и дёргать её по cron'у.

    Альтернативно, если вы используете Symfony 4.4 или 5.x - вы можете организовать этот процесс через новый Symfony Messenger. Он же будет лучшим вариантом если, к примеру, запросов много или они тяжёлые и вам нужно организовать их распределение.
    Ответ написан
    Комментировать
  • Как протестировать метод контроллера в Symfony5?

    glaphire
    @glaphire
    PHP developer
    Раньше с таким не сталкивалась, но есть момент в описании вопроса - просто сделать эхо контента и exit не выйдет, чтобы вывести файл нужно еще добавить заголовки контента, размер файла и способ вывода. Т.е. в таком виде как метод контроллера есть сейчас я не понимаю, как он может корректно работать)
    Даже если без доработок, то для предотвращения вывода контента файла в консоль достаточно использовать ob_start() и ob_get_clean().

    Доработала пример:
    Контроллер:
    <?php
    
    namespace App\Controller;
    
    use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
    use Symfony\Component\HttpFoundation\BinaryFileResponse;
    use Symfony\Component\Routing\Annotation\Route;
    
    class TestController extends AbstractController
    {
        /**
         * @Route("/get-image", name="get_image")
         */
        public function getImage()
        {
            $path = "/home/dariia/Code/sf5_http_kernel_course/public/uploads/article_image/lightspeed-5f687f2402b6f.png";
    
            $response = new BinaryFileResponse($path);
    
            return $response;
        }
    }


    Тест:
    <?php
    
    namespace App\Tests\Controller;
    
    use Symfony\Bundle\FrameworkBundle\Test\WebTestCase;
    
    class TestControllerTest extends WebTestCase
    {
        public function testRegister()
        {
            $client = static::createClient();
            ob_start();
            $client->request('GET', '/get-image');
            $client->getResponse()->sendContent();
            $this->assertResponseIsSuccessful();
            $this->assertResponseHeaderSame('Content-Type', 'image/png');
            ob_get_clean();
    ...

    Дальше можно добавить ассерты на тему размера файла, имени и т.д., просто нужно иметь какую-то привязку к файлу, чтобы этими данными манипулировать, а я упростила пример)
    Ответ написан
    5 комментариев
  • Как использовать PDO в других классах?

    FanatPHP
    @FanatPHP
    Чебуратор тега РНР
    Вопрос хороший и правильный. Всё остальное - не очень.
    К сожалению, то что у тебя сейчас - это тоже говнокод и неправильно.

    Тут надо понимать одну очень простую, но очень неприятную вещь: ООП, в отличие от ПХП - это сложная тема. Явочным порядком, там подглядев, тут скопипастив - ООП изучить нельзя.
    Максимум что у тебя получится - это та же процедурщина, вид сбоку. Неподдерживаемый говнокод, просто по-обезьяньи обернутый в классы.

    Но учиться все равно надо.
    Главное что надо понимать про ООП - оно не про сами классы, а про их взаимодействие.
    именно поэтому то самое жлобал и является говнокодом. Потому что никакого взаимодействия нет ,и класса тоже нет - есть функция, которую если вынуть из класса, то НИЧЕГО не изменится.

    Но и то что у тебя сейчас - это не ООП.
    Каждый раз создавать новое подключение можно и без всякого ООП. Вот только оно убьёт тебе сервер БД.
    Чтобы было ООП, надо передать уже созданный инстанс класса для работы с БД как параметр конструктора.

    Кроме того, класс qpdo - это какой-то анекдот, бессмыслица. Обезьяна увидела как человек носит очки, напялила на нос ложку и ходит с гордым видом. Внешне вроде то же самое, но смысла никакого.
    Ты можешь объяснить, ЗАЧЕМ тебе класс qpdo? Чтобы настройки прописать? А ты подумал что настройки бывают РАЗНЫЕ? Что дома у тебя логин рут и пароль пустота, а на хостинге это не прокатит. И что - будешь код переписывать, каждый раз заливая из дома на сервер? Серьёзно?
    Настройки должны всегда лежать отдельно. А больше ни для чего твой класс не нужен.

    Поэтому,

    1. qpdo выкинуть на помойку, по крайней мере до тех пор, пока не поймешь, для чего тебе свой класс, и как с ним обращаться.
    2. Создать один раз инстанс класса для работы с БД (в простейшем случае - PDO) и передавать в другие классы через констркутор
    3. в конструкторе присваивать его переменной класса, которую использовать для доступа к БД.

    В итоге возвращаемся к исходному вопросу, как передать соединение в другой класс:
    class somethingClass {
      function __construct($db) {
        $this->db = $db;
      }
    
      function somethingFunction(string $key) {
        $query = $this->db->prepare("SELECT `key` FROM `table` WHERE `key` = :key");
        $query->execute(array(':key' => $key));
        return $query->fetchColumn();
      }
    }


    Бонус
    echo 'Ошибка подключения к БД MySQL: ' . $error->getMessage() . ''; die;
    - это ад и говнокод.
    Да, в прошлом веке так писали. С тех про прошло уже 20 чертовых лет. 20, Карл! Тебя небось еще в проекте не было.
    Сайт, который вываливает вот это всё на всеобщее обозрение - это позор.
    Все ошибки сайт должен всегда держать при себе. И никому не показывать. И поэтому никогда не надо лезть руками в ту ошибку, которую выбрасывает РНР. Надо дать ей спокойно пойти туда, куда идут все остальные ошибки.
    Ответ написан
    Комментировать
  • Как в симфони делить проект по NameSpace?

    glaphire
    @glaphire
    PHP developer
    Шарить между проектами - делать как бандл/композеровский пакет.
    Просто использовать в проекте - создавать условно < project name >/src/Module/< module name> и всю логику писать в пределах App\Module\ModuleName.
    Ответ написан
    3 комментария
  • Как победить Undefined index в UnitOfWork при сохранении внутри postRemove?

    @Flying
    В документации к событию postRemove прямо указано что это событие вызывается внутри метода flush(). Таким образом вызывая $this->entityManager->flush() внутри postRemove вы, фактически порождаете потенциально бесконечный цикл, поэтому "Undefined index" на самом деле - наименьшая из ваших проблем :)

    Более корректно будет организовать работу примерно следующим образом:
    1. Вынести логику сохранения изменений в отдельный сервис
    2. Обернуть процесс сохранения изменений в транзакцию
    3. В lifecycle методах не пытаться писать данные сразу, а вместо этого собирать информацию для записи в некую коллекцию, в простейшем случае - массив
    4. После основного flush'а проверять содержимое коллекции и если там что-то есть - формировать отдельный набор изменений в entities и делать новый flush.
    5. Если всё прошло хорошо - в конце делать commit транзакции
    Ответ написан
    7 комментариев
  • Как исправить ошибку при создании команды?

    Maksclub
    @Maksclub
    maksfedorov.ru
    Это значит, что через контейнер вы не передали в команду аргумент

    В команду сам контейнер может автоматом подставить аргументы, если это другие сервисы и одновременно FQCN, в случае примитивов — значения или явно передайте или нужно забиндить
    Ответ написан
    Комментировать
  • Актуален ли Yii2 Фреймворк в 2020?

    myks92
    @myks92 Куратор тега Yii
    Нашёл решение — пометь вопрос ответом!
    Тут много нюансов и ответить однозначно сложно. Как уже написал Иван Шумов фреймворк - это инструмент. Каждый инструмент подбирается под конкретную задачу:

    • СMS - система управления сайтом (Wordpress, Joomla и т.д.). Создан для простого и удобного управления сайтом (контентом). Как правило, доступен для работы простому пользователю, без знания языков программирования. Подойдёт для: простых сайтов, блогов, там где не требуется нагрузка и безопасность.
    • RAD framework (Yii2) - фреймворк для быстрой разработки. RAD Фреймворк имеет, монолитную и связанную архитектуру. Он является антипаттенрном и позволяет вам делать всё что угодно, лишь бы вы быстро собрали свой проект. (Используется паттерн Table First - спроектировали базу и по схеме сгенерировали модели данных). При этом Вам никто не говорит, что он не может использоваться для серьёзных проектов. Например, DNS..
    • Компонентные фреймворки (Symfony, Laravel). Это фреймворки, которые разделены на независимые компоненты, которые вы можете подключать к своему проекту при необходимости. В самом фреймворке заложен только необходимый минимум, а всё остальное вынесено в компоненты. Эти компоненты могут использоваться (или не использоваться). Могут использоваться для другого фреймворка (сам Yii2 использует компоненты симфони). Doctirine, которая позволяет вам забыть о базе данных и сосредоточиться на коде. Фреймворк задаёт вам некий стиль, некую структуру и типизацию и требует более хорошего и обдуманного кодинга. Например, при использовании twig вы не сможете сделать запрос в view. Чего нельзя сказать про Yii. Вы там можете построить хорошую бизнес логику, что и встречается...
    • Микрофреймворки (Slim). В таких фреймворках заложен самый минимум, а всё остальное программист должен искать или писать самостоятельно, выстраивать свою архитектуру приложения. Он легкий и простой. Кто-то и Symfony относит к микрофреймворкам. Тут меня многие могут упрекнуть. Подойдет для очень простых, гибких, легких приложений, например, микросервисов. Фреймворк не задает вам никаких правил и архитектуры. Вы должны сами продумать все детали приложения. Где-то используете twig, где то Data Mapper, где-то ActiveRecord. Для работы с таким фреймворком требуется много знаний и опыта. Так как вам всё придется делать самому. Здесь за вас никто не подумал.

    Что же для чего выбрать?
    Решать вам исходя из конкретной задачи. Здесь нет понятия хуже/лучше. Чтобы что-то выбрать нужно на каждом попробовать собрать пару проектов. Тогда и будет понимание когда и что выбирать. Это как автомобиль. Какой лучше BMW или LADA? Оба автомобиля, но каждый для своей аудитории, по своим потребностям, для своих задач.

    Почему другие говорят что Yii2 плохой?
    • 2 версия морально устарела. Это не значит что ВЕСЬ фреймворк плохой, просто им давно никто не занимался. Сейчас идет разработка Yii3, которая многократно лучше 2 версии, но 3 версия доступна только в демо. Поэтому, если хочется писать НОВЫЙ проект на Yii, то я бы не стал выбирать 2 версию. Можете подождать Yii3, но никто не знает когда будет релиз. Можете начать писать используя Yii3-demo, но там может всё поменяться к моменту релиза.
    • Он монолитный. Если вам требуется сделать проект используя много приложений с разными компонентами, то пойдёте по пути Advanced шаблона, где каждое приложение будет папкой: backend, forntend, console. Однако при большой нагрузке вам потребуется разносить приложения. Тогда ваш core будет дублироваться во всех приложениях. Там будет то, что вам даже не требуется. Потому что Yii2 монолит.
    • В Yii2 используется Actvie Record. AR - это не плохо. Его многие любят, многие ценят и хвалят. Но для больших проектов он предоставляет неудобства. Например, при изменении поля в базе данных - приходится менять все свойства и надеяться что везде поменяли, так как в GrigView используется магия, при получение значений из модели 'value' => 'profile.last_name'. Так же нельзя сделать свойства модели (сущности), приватными. Из-за чего состояние модели можно изменить где угодно и вы никак не проконтролируете это.
    • Много абстракции, которую не распознает редактор. Например, вы можете вызвать в контроллере Yii::$app->user->id. Вам приходится делать докблок, что User это common/auth/Identity, а не yii/web/User


    Ну и так далее. С каждым из этих пунктов можно бороться, при правильном подходе. Как уже сказали в плохих руках любой код может пахнуть. Но это основное. Надеюсь, меня не раскритикуют другие за спорные моменты. В таких вопросах всегда есть разногласия) Думаю, что смог донести в сжатом виде отличия.
    Ответ написан
    4 комментария