Блог Андрея

 
 

FAQ по работе с Symfony 3 и Symfony 4 на localhost xubuntu 18.04

FAQ по работе с Symfony 3 и Symfony 4 на localhost xubuntu 18.04

Заметил что мой FAQ по установке Symfony 3 в 2019-ом году перерос в FAQ по разработке.

Поэтому на вопросы, возникшие у меня при изучении этого чуднОго фреймвёрка в 2019-ом году и отнявшие достаточно много времени выношу сюда.

24.10.2019 18:23 Как с помощью QueryBuilder сделать LEFT JOIN ON? (приложение Symfony 3.4 сгенерировано из консоли Symfony cli 4.3)?

Решение:

repository = $this->getDoctrine()->getRepository('App:Main');
		$qb = $repository->createQueryBuilder('m');
		
		/*$aWhere = [
			'isDeleted' => 0,
			'isHide' => 0,
			'isModerate' => 1
		];*/
		$oQuery = $qb->select('m.id, m.title, m.addtext, m.price, m.people, m.box, m.term, m.far, m.near, m.piknik, m.image, m.name, m.phone, m.pinned, m.codename')
			->where( $qb->expr()->eq('m.isDeleted', 0) )
			->where( $qb->expr()->eq('m.isHide', 0) )
			->where( $qb->expr()->eq('m.isModerate', 1) )
			->leftJoin('AppEntityCities', 'c', 'WITH', 'c.id = m.city')
			->addSelect('c.cityName')
			//->leftJoin('App:Regions', 'r', 'on', 'r.id = m.region')
			->getQuery();
		
		$aCollection = $oQuery->getResult();
		var_dump($aCollection);
		die;/**/

Что ещё за WITH спросил я себя, ибо могу сознаться до сих пор не знал этого. Потратив кучу времени и нервов включил generel log на mysql сервере и увидел там запрос:

		    3 Query	SELECT m0_.id AS id_0, m0_.title AS title_1, m0_.addtext AS addtext_2, m0_.price AS price_3, m0_.people AS people_4, m0_.box AS box_5, m0_.term AS term_6, m0_.far AS far_7, m0_.near AS near_8, m0_.piknik AS piknik_9, m0_.image AS image_10, m0_.name AS name_11, m0_.phone AS phone_12, m0_.pinned AS pinned_13, m0_.codename AS codename_14, c1_.city_name AS city_name_15 
FROM main m0_ 
LEFT JOIN cities c1_ 
ON (c1_.id = m0_.city) -- !!! @#%^!!!
 WHERE m0_.is_moderate = 1

То есть:

Запрос, похожий на SQL, который вы можете видеть в выводе ошибок и дебаге Symfony - это не SQL запрос.

Как я понял, люди разработавшие Doctrine 2 придумали инструкцию WITH, чтобы можно было писать, как я написал в примере, а

->leftJoin('AppEntityCities', 'c', 'ON', 'c.id = m.city')
оно как бы есть, но не работает, выводя однако на страницу запрос в котором фигурирует ON.

Вообще они придумали классно, читая аннотации построитель запросов должен был сам там чего-то прицепить, но добиться этого мне за два часа не удалось. Хотя аннотации @OneToMany @ManyToMany у меня прописаны и используя findBy я видел, что они работают корректно. Возможно, это не они. Завтра попробую объект Criteria передать в findBy, вместо массива aWhere если сработает, сэкономит время.

24.10.2019 18:23 Как в Doctrine 2 Repository::findBy передать условие, содержащее OR? (приложение Symfony 3.4 сгенерировано из консоли Symfony cli 4.3)?

Решение:

Никак. Мозг не хочет с этим мирится, но это правда так. Если вам требуется запрос вроде SELECT t.name FROM t WHERE t.id = 10 OR t.parent_id = 50; вы не можете задать его условие с помощью массива, который передается в Repository::findBy(); первым аргументом. Выход - использовать конструктор запросов или Criteria.

Аналог метода, реализующего с помощью Repository::findBy SQL запрос вида

SELECT * FROM t WHERE x=1 AND y = 1 AND z = 1 AND a = 1 AND b = 1 AND c = 1
приведён ниже:

    /**
	 * 
	 * @param string $sRegion = '' код региона латинскими буквами
     * @param string $sCity = ''   код города латинскими буквами
	 * @return array
	*/
	private function _loadAdvList(string $sRegion = '', string $sCity = '', Request $oRequest) : array
	{
		$limit = $this->getParameter('app.records_per_page', 10);
		$aWhere = [
			'isDeleted' => 0,
			'isHide' => 0,
			'isModerate' => 1
		];
		if ($sRegion) {
            //Тут может ещё несколько элементов в $aWhere добавиться 
            //в зависимости от http запроса
			$this->_setCityConditionAndInitCyrValues($aWhere, $sRegion, $sCity);
		}
		
		$oSession = $oRequest->getSession();
		if (intval($oSession->get('people', 0))) {
			$aWhere['people'] = 1;
		}
		if (intval($oSession->get('box', 0))) {
			$aWhere['box'] = 1;
		}
		if (intval($oSession->get('term', 0))) {
			$aWhere['term'] = 1;
		}
		if (intval($oSession->get('far', 0))) {
			$aWhere['far'] = 1;
		}
		if (intval($oSession->get('near', 0))) {
			$aWhere['near'] = 1;
		}
		if (intval($oSession->get('piknik', 0))) {
			$aWhere['piknik'] = 1;
		}
		
		$repository = $this->getDoctrine()->getRepository('App:Main');
		$aCollection = $repository->findBy($aWhere, [
			'delta' => 'DESC',
		], $limit, 0);
		
		return $aCollection;
	}

Если понадобится немного изменить запрос, чтобы он приобрёл вид

SELECT * FROM t WHERE x=1 AND (y = 1 OR z = 1) AND (a = 1 OR b = 1 OR c = 1)
можно использовать такой код с QueryBuilder:

/**
	 * 
	 * @param string $sRegion = '' код региона латинскими буквами
     * @param string $sCity = ''   код города латинскими буквами
	 * @return array
	*/
	private function _loadAdvList(string $sRegion = '', string $sCity = '', Request $oRequest) : array
	{
		$limit = $this->getParameter('app.records_per_page', 10);
		$repository = $this->getDoctrine()->getRepository('App:Main');
		$oQueryBuilder = $repository->createQueryBuilder('m');
		$oQueryBuilder = $oQueryBuilder->select()
			->where( $oQueryBuilder->expr()->eq('m.isDeleted', 0) )
			->andWhere( $oQueryBuilder->expr()->eq('m.isHide', 0) )
			->andWhere( $oQueryBuilder->expr()->eq('m.isModerate', 1) )
			->orderBy('m.delta','DESC')
			->setMaxResults($limit)
			->setFirstResult(0);

		if ($sRegion) {
            //Тут в зависимости от http запроса может быть ещё пару раз вызван
            //$oQueryBuilder->andWhere();
			$this->_setCityConditionAndInitCyrValues($oQueryBuilder, $sRegion, $sCity);
		}
		
		$oSession = $oRequest->getSession();
		$aOrWhereType = [];
		$aOrWhereDistance = [];
		if (intval($oSession->get('people', 0))) {
			$aOrWhereType[] = 'm.people = 1';
		}
		if (intval($oSession->get('box', 0))) {
			$aOrWhereType[] = 'm.box = 1';
		}
		if (intval($oSession->get('term', 0))) {
			$aOrWhereType[] = 'm.term = 1';
		}
		if (intval($oSession->get('far', 0))) {
			$aOrWhereDistance[] = 'm.far = 1';
		}
		if (intval($oSession->get('near', 0))) {
			$aOrWhereDistance[] = 'm.near = 1';
		}
		if (intval($oSession->get('piknik', 0))) {
			$aOrWhereDistance[] = 'm.piknik = 1';
		}
		if ($aOrWhereType) {
            //Добавляем первые скобки с OR
			$oQueryBuilder->andWhere($oQueryBuilder->expr()->andX(   join(' OR ', $aOrWhereType) ) );
		}
		if ($aOrWhereDistance) {
            //Добавляем вторые скобки с OR
			$oQueryBuilder->andWhere($oQueryBuilder->expr()->andX(   join(' OR ', $aOrWhereDistance) ) );
		}
		
		$aCollection = $oQueryBuilder->getQuery()->execute();
		return $aCollection;
	}

25.10.2019 14:59 Как в Doctrine 2 QueryBuilder передать условие, содержащее '... AND ... ( ...OR ... OR ...)' ? (приложение Symfony 3.4 сгенерировано из консоли Symfony cli 4.3)?

Решение:

    /**
     * @Route("/training/andoror", name="training_andoror")
    */
    public function testAndOrWhere()
    {
        //SELECT * FROM main WHERE (people = 1 OR box = 1) AND (near = 1 OR far = 1)
        $oRepository = $this->getDoctrine()->getRepository('App:Main');
        $oQueryBuilder = $oRepository->createQueryBuilder('m');
        $orCond1 = $oQueryBuilder->expr()->orX();
        $orCond2 = $oQueryBuilder->expr()->orX();
        $orCond1->add($oQueryBuilder->expr()->eq('m.people', 1));//совсем по фэншую!
        $orCond1->add('m.box = 1');//не совсем по феншую
        $orCond2->add('m.near = 1');
        $orCond2->add('m.far = 1');
        $oAndCond = $oQueryBuilder->expr()->andX();
        $oAndCond->add($orCond1);
        $oAndCond->add($orCond2);
        $oQueryBuilder->andWhere($oAndCond);
        $oQuery = $oQueryBuilder->getQuery();
        $aResult = $oQuery->execute();
        var_dump($aResult);
        die;
    }

Также смотрите тут пример с QueryBuilder

30.10.2019 15:15 Как в Doctrine 2 выполнить Native SQL запрос с GROUP_CONCAT и получить в результате ассоциативный массив? (приложение Symfony 3.4 сгенерировано из консоли Symfony cli 4.3)?

Решение:
Насколько я понял "философию" Symfony, так делать нельзя. Используйте "нативный" sql в самых крайних случаях, а лучше не используйте его вовсе.

В Symfony 3 всё оказалось очень просто:

    /**
     * @Route("/training/groupconcat", name="training_groupconcat")
     */
    public function groupconcat()
    {
    //Нужно выполнить:
        /*SELECT m.phone, GROUP_CONCAT(m.title, ';;;') AS titles, GROUP_CONCAT(m.idm, ';;;') AS idlist FROM adverts AS m 
					GROUP BY (m.phone)*/
        	//Делаем в контроллере:
        $oEm = $this->getDoctrine()->getEntityManager();
	    $sQuery = 'SELECT m.id, m.phone, GROUP_CONCAT(m.title) AS titles, GROUP_CONCAT(m.id) AS idlist FROM adverts AS m 
					GROUP BY (m.phone)';
		$statement = $oEm->getConnection()->prepare($sQuery);
        $statement->execute();
        //Если бь надо было передать параметр, например у нас такой запрос
        //$sQuery = 'SELECT * FROM my_table where my_table.field = :status LIMIT 5;';
        //$statement = $oEm->getConnection()->prepare($sQusery);
        //Делаем так:
        //$statement->bindValue('status', 1);

        //ну а в нашем случае можно сразу
       $aResult = $statement->fetchAll();
	    var_dump($aResult);
	    die;
    }

Но, повторюсь, это неверный подход. К тому же он по слухам не работает в Symfony 4 (хотя работает в приложении Symfony 4.3.5 созданным из консоли командой symfony new hellos4 --full).

Правильным подходом было бы установить

composer require beberlei/doctrineextensions

Чтобы не дописывать автозагрузку вручную, как прописано у них в readme, достаточно отредактировать файл config/packages/doctrine.yaml и дописать в него в секцию orm:

    dql:
              string_functions:
                group_concat: DoctrineExtensions\Query\MysqlGroupConcat

И использовать QueryBuilder:

    /**
     * @Route("/training/groupconcat", name="training_groupconcat")
    */
    public function groupconcat()
    {
        /*SELECT m.phone, GROUP_CONCAT(m.title, ';;;') AS titles, GROUP_CONCAT(m.id, ';;;') AS idlist FROM adverts AS m 
					GROUP BY (m.phone)*/
        $oEm = $this->getDoctrine()->getRepository('App:Adverts');
        $oQueryBuilder = $oEm->createQueryBuilder('m');
        $oQueryBuilder->select("m.phone, GROUP_CONCAT(m.title, ';;;') AS titles, GROUP_CONCAT(m.id, ';;;') AS idlist")
			->groupBy('m.phone');
		$oQuery = $oQueryBuilder->getQuery();
		$aResult = $oQuery->getResult();
		var_dump($aResult);
		die;
    }

Для любопытных.

Те, кто интересуется, можно ли было использовать EntityManager::createNativeQuery() могут читать далее.

Строго говоря, да можно. Но лучше бы было нельзя. Результат EntityManager::createNativeQuery()->getResult() вернёт массив тех сущностей, которые вы зададите с помощью экземпляра объекта класса Doctrine\ORM\Query\ResultSetMapping.

Но так как у вас нет подходящего класса в src/Entity (подходящий в нашем случае тот, который содержал бы поля phone, titles, idlist), мы можем либо смапить результат GROUP_CONCAT в какие-то строковые поля любого класса из каталога Entity, который имеет три строковых поля, либо создать новую сущность, в которой объявить все необходимые поля. Вот тут (метод groupconcat) я так и сделал, но это оказалось плохой практикой, так как перестало выполняться

php bin/console doctrine:schema:update --force

и я в итоге вернулся к варианту с QueryBuilder. Мапить же в существующую сущность (например я мог смапить в EntityMain так как там есть строковые поля title и image и поместить результат конкатенации title в title, а результат конкатенации в id в image) можно и это работает. Однако работать с результатом не очень удобно, а если бы мне требовался в результате ещё и массив путей к изображениям, это поставило бы крест на использовании EntityManager::createNativeQuery().

Итак, единственный адекватный способ использовать GROUP_CONCAT - это использовать QueryBuilder и расширение beberlei/DoctrineExtensions

02.11.2019 20:34 Как сделать, чтобы один и тот же запрос к базе данных не выполнялся два раза подряд? (приложение Symfony 3.4 сгенерировано из консоли Symfony cli 4.3)?

Решение:

Смотрите тут

06.11.2019 16:58 Как кэшировать результаты запросов к базе данных в Memcache? (приложение Symfony 3.4 сгенерировано из консоли Symfony cli 4.3)?

Решение:

Смотрите тут

09.11.2019 13:24 Как сконфигурировать swiftmailer в .env файле? (приложение Symfony 3.4 сгенерировано из консоли Symfony cli 4.3)?

Решение:

Смотрите тут

09.11.2019 13:24 Как сделать авторизацию пользователей в проекте Symfony? (приложение Symfony 3.4 сгенерировано из консоли Symfony cli 4.3)?

Решение:

Статья про установку в Symfony 4 оказалась даже полезней документации на гитхабе соответствующего пакета.

А те, кто не хочет использовать сторонний пакет, могут почитать тут.

12.11.2019 18:02 Как добавить google re-captcha на форму сброса пароля по email в Symfony? (приложение Symfony 3.4 сгенерировано из консоли Symfony cli 4.3)?

Решение:

перегружаем (декорируем) контроллер и добавляем каптчу

15.11.2019 18:02 Как переопределить FormType для FOSUserBundle в Symfony? (приложение Symfony 3.4 сгенерировано из консоли Symfony cli 4.3).

Решение:

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

16.11.2019 18:02 Как сделать надписи справа от чекбоксов на всех формах в Symfony? (приложение Symfony 3.4 сгенерировано из консоли Symfony cli 4.3).

Речь идёт о чекбоксах, которые генерируются при использовании FormBuilderInterface.

Решение:

В этом коммите видно, что это очень просто, если знать как.. Создана тема, кастомизирован block form_row, тема применена ко всем формам проекта в конфигурации config/packages/twig.yaml. Все остальные блоки, понятное дело, берутся из темы по умолчанию.

Интересно, что если вы создаёте форму сами, а не используете готовую из стороннего бандла для этого можно было использовать ChoiceType с expanded = true и multiple = true состоящий из одного элемента.

16.11.2019 18:02 Как оформить свой бандл в Symfony как пакет композер? (приложение Symfony 3.4 сгенерировано из консоли Symfony cli 4.7).

Решение:

Я решил создать набор консольных команд для Symfony чтобы облегчить жизнь себе, а если получится и другим людям. Так как я хочу, чтобы они запускались командой вида php bin/console landlib:decorate-controller мне нужно создать команду Symfony. А так как я хочу использовать их повторно (в других проектах Symfony) мне нужен бандл, который можно установить через composer. Его создание опишу тут.

18.11.2019 18:35 Как используя FOSUserBundle сделать ajax обработку формы логина? (приложение Symfony 3.4 сгенерировано из консоли Symfony cli 4.7).

Решение:

В принципе оно тут.

Мне понадобилось лишь немного повозиться с получением сессии в конструкторе AuthenticationHandler, потому как Symfony обновилось. Мой вариант в этом коммите и вот в этом.

20.11.2019 16:28 Какие хэндлеры (Handlers) существуют в Symfony 3.4 или как что-то сделать в Symfony в момент logout-а? (приложение Symfony 3.4 сгенерировано из консоли Symfony cli 4.7).

Решение:

Данным вопросом озаботился решая вот этот. В ходе его решения узнал о существовании AuthenticationSuccessHandlerInterface и AuthenticationFailureHandlerInterface. Заинтересовался, какие ещё хэндлеры можно определить в приложении Symfony и все ли они связаны с безопасностью. Хэндлеры по своей сути напоминают обработчики событий, но это не те события, с которыми мы работаем объектами класса EventDispatcher. Выполнив в проекте поиск файлов по маске *HandlerInterface.php я нашел 11 файлов.

Из них 5 относятся к security, один к формам, три к monolog, и по одному к отладке и css - парсеру.

... \Form\RequestHandlerInterface.php судя по всему нельзя (или незачем) определить так, чтобы всякий запрос им обрабатывался. Поиском на Github удалось найти только инициализацию переменной этого типа объектом класса HttpFoundationRequestHandler.

Добиться толку пока удалось только от LogoutHandlerInterface.

Для этого в конфиге services.yml определил:

    app.security.logout_handler:
        class: App\Handler\LogoutHandler
        public: false
        arguments: ["@service_container"]

а в конфиге security.yaml заменил

logout: true

на

logout:
                handler: app.security.logout_handler

Всё просто, единственный минус - пришлось некоторое время гадать, как именно назвать ключ. Я его поначалу пытался назвать logout_handler вместо logout. Хорошо бы понять, как избавиться от таких гаданий.

Ну и в папке App\Handler создал класс LogoutHandler реализующий SymfonyComponentSecurityHttpLogoutLogoutHandlerInterface. Тут уж совсем просто, берешь методы из файла интерфейса и реализуешь в зависимости от потребностей бизнес-логики.

21.11.2019 17:19 Как в Symfony 3.4 создать переменную доступную в любом twig шаблоне? (приложение Symfony 3.4 сгенерировано из консоли Symfony cli 4.7).

Решение:

В случае, если во всех twig шаблонах должна быть доступна по сути дела константа, всё довольно просто. Мы можем определить это значение в yaml конфигурации:

twig:
	globals:
	   x: 15
       y: "%app.y%"

Тогда в services.yaml (для переменной y) должно быть

parameters:
    app.y: 159

или

parameters:
    app.y: "%env(resolve:DOT_ENV_FILE_PARAMETER_Y)%"

Обратите внимание, что пример для переменнной y предпочтительнее, если нам какое-то значение нужно и в контролллере и в шаблоне: тогда в контроллерах и сервисах мы можен использовать

$this->container->getParameter('app.y');

а в twig шаблоне

{ y }

(и если нам придётся изменить значение y мы сделаем это отредактировав всего один файл всего в одном месте!).

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

Допустим, я хочу всегда иметь доступ к javascript переменной window.UID = { uid }; - 0 когда пользователь неавторизован и иметь в uid идентификатор пользователя, когда он авторизован.

Событие ядра Symfony ( onKernelResponse(FilterResponseEvent $event); ) казалось прекрасной возможностью сделать это, но к сожалению мы можем в нём перехватить объект Response, когда он уже сформировал html контент. Можно конечно в моём частном случае использовать str_replace (чтобы заменить строку 'window.UID = 0;' на 'window.UID = X;') - но это тяжеловато, заставлять сервер каждый раз весь контент страницы прогонять через str_replace. Это вредно в плане скорости формирования ответа сервера.

Пока я использовал только собственные контроллеры, я выходил из положения создав сервис с методом getDefaultViewData() - в котором формировал массив с переменными для master-шаблона сайта. Но как только я стал использовать сторонний бандл (речь конкретно о бандле FOSUserBundle) при темизации его форм я тут же наткнулся на ошибку неопределённой переменной Twig uid. Причем если для большинства форм я смог справиться с ситуацией просто определив её глобально со значение 0, то на странице редактирования профиля мне потребовалось (из -за особенностей ТЗ) знать, авторизован пользователь или нет.

В Symfony есть лазейка, можно было сделать в master шаблоне так:

{% if (app.user) %}}
    <script>var uid = '{ app.user.id }';</script>
{% else %}
    <script>var uid ='0';</script>
{% endif %}}

но я вижу тут две проблемы.

Во-первых, это логика в шаблоне. Что очень не по феншую, я сейчас не про фэншуй Symfony, а про фэншуй MVC.

Во вторых, а что делать если мне нужно какое-то более сложное вычисляемое значение? Для которого нет готового объекта, к которому можно также легко и просто обратиться как к app.user?

Я предпочёл определить twig фильтр get_uid (как определять свои фильтры для twig довольно хорошо описано в массе источников, поэтому я не буду тут повторяться)

Теперь я могу написать

<script>var uid = '{ 0|get_uid }';</script>

что весьма сократило объём кода шаблона, некая логика в нём по-прежнему есть (передаём в фильтр get_uid 0, а реализация фильтра на самом деле игнорирует иэтот параметр и возвращает мне user_id в зависимости от того, авторизован ли пользователь).

24.11.2019 10:49 Symfony 3.4 как правильно должен называтсья firewall, main или secured_area? (приложение Symfony 3.4 сгенерировано из консоли Symfony cli 4.7).

Решение:

тут

25.11.2019 10:49 Symfony 3.4 как сделать авторизацию без использования FOSUserBundle? (приложение Symfony 3.4 сгенерировано из консоли Symfony cli 4.7).

Решение:

тут и тут

02.12.2019 12:50 Symfony 3.4 как загрузить изображение и изменить его размер? (приложение Symfony 3.4 сгенерировано из консоли Symfony cli 4.7).

Решение:

тут.

26.12.2019 12:06 Symfony 3.4 как сделать так, чтобы только часть сайта была на фреймвёрке? ( приложение Symfony 3.4 сгенерировано из консоли Symfony cli 4.7).

Решение:

тут.

31.01.2020 15:01 Symfony 3.4 - не работает в окружении prod, ошибка «excluded_http_codes» cannot be used as your version of Monolog bridge does not support it.

( приложение Symfony 3.4 сгенерировано из консоли Symfony cli 4.7).

Решение:

тут.