Глупые ошибки бывают и в популярных фреймворках
На днях был случай — упала ошибка на одном из проектов с Laravel 6. По стектрейсу понятно, что проблема находится в вендорском коде, а именно в /vendor/Illuminate/Cookie/Middleware/EncryptCookies.php. Полез туда, в метод
Справедливости ради отмечу, что в восьмой версии фреймворка этот метод починили, добавив в первый
Я воспользовался тем, что это middleware наследуется в пользовательском коде (
На днях был случай — упала ошибка на одном из проектов с Laravel 6. По стектрейсу понятно, что проблема находится в вендорском коде, а именно в /vendor/Illuminate/Cookie/Middleware/EncryptCookies.php. Полез туда, в метод
decrypt(), который выглядит следующим образом./**Ошибка в той строчке, которая начинается с
* Decrypt the cookies on the request.
*
* @param \Symfony\Component\HttpFoundation\Request $request
* @return \Symfony\Component\HttpFoundation\Request
*/
protected function decrypt(Request $request)
{
foreach ($request->cookies as $key => $cookie) {
if ($this->isDisabled($key)) {
continue;
}
try {
$value = $this->decryptCookie($key, $cookie);
$hasValidPrefix = strpos($value, CookieValuePrefix::create($key, $this->encrypter->getKey())) === 0;
$request->cookies->set(
$key, $hasValidPrefix ? CookieValuePrefix::remove($value) : null
);
} catch (DecryptException $e) {
$request->cookies->set($key, null);
}
}
return $request;
}
$hasValidPrefix =..., первый аргумент strpos() неправильного типа. Первый аргумент — $value. Сигнатура strpos совершенно недвусмысленна: strpos(string $haystack, string $needle, int $offset = 0): int|false, $value должно быть строкой, но ей не является. Поскольку $value получается из метода decryptCookie(), стоит взглянуть туда. И действительно, в аннотации указано:/**т.е. он может возвращать и массив. На месте же возвращаемое значение на массив нигде не проверяется, потому-то и возникает ошибка типа.
* Decrypt the given cookie and return the value.
*
* @param string $name
* @param string|array $cookie
* @return string|array
*/
Справедливости ради отмечу, что в восьмой версии фреймворка этот метод починили, добавив в первый
if() проверку на массив, так что если кука — массив, до проблемного места выполнение в принципе не доходит. Но мне надо было исправить в шестой версии. Я воспользовался тем, что это middleware наследуется в пользовательском коде (
App\Http\Middleware\EncryptCookies), скопировал туда родительский метод и перенёс исправление из 8-й версии. Проблема решена.GitHub
framework/EncryptCookies.php at 6.x · laravel/framework
The Laravel Framework. Contribute to laravel/framework development by creating an account on GitHub.
Как работать с деревом элементов, хранимых в плоском виде в базе данных?
Такая задача часто встаёт перед разработчиками. Представим себе примерную структуру таблицы
Какие операции могут требоваться от такого дерева?
1) Получение родителя для данного элемента
2) Получение потомков для данного элемента
3) Получение всех нижележащих элементов, включая вложенные
4) Получение всех предков данного элемента (например, для хлебных крошек)
5) Получение элементов-братьев данного элемента
6) Работа с уровнем вложенности
и так далее. При прямой работе с БД (или даже через ORM) каждая из этих задач, не считая первых двух, требует минимум нескольких дополнительных запросов. Например, братья получаются так (
Возникает вопрос: можно ли как-то не делать все эти дополнительные запросы? Как оказывается, можно. При создании класса я исходил из следующих предпосылок:
1) элементы должны получаться однажды;
2) перебор всех элементов тоже должен происходить однажды, и тогда же выстраиваться внутренняя структура дерева.
Для каждого элемента выстраивается полный путь в виде строки из серии
Все операции над деревом выполняются путём оперирования с массивом путей
Такая задача часто встаёт перед разработчиками. Представим себе примерную структуру таблицы
items, в которой хранятся элементы дерева.INTEGER idДополнительные поля могут присутствовать, но сейчас они нам не так интересны.
VARCHAR name
INTEGER parent
Какие операции могут требоваться от такого дерева?
1) Получение родителя для данного элемента
2) Получение потомков для данного элемента
3) Получение всех нижележащих элементов, включая вложенные
4) Получение всех предков данного элемента (например, для хлебных крошек)
5) Получение элементов-братьев данного элемента
6) Работа с уровнем вложенности
и так далее. При прямой работе с БД (или даже через ORM) каждая из этих задач, не считая первых двух, требует минимум нескольких дополнительных запросов. Например, братья получаются так (
$target — целевой элемент, $parent — получаемый первым запросом родительский):SELECT id, name, parent FROM items WHERE id = {$target->parent}
SELECT id, name, parent FROM items WHERE parent = {$parent->id} AND id != {$target->id}
Вместо двух запросов можно сделать один со вложенным внутри, но суть от этого не меняется. В остальных случаях ситуация хуже (особенно по пункту 3). Возникает вопрос: можно ли как-то не делать все эти дополнительные запросы? Как оказывается, можно. При создании класса я исходил из следующих предпосылок:
1) элементы должны получаться однажды;
2) перебор всех элементов тоже должен происходить однажды, и тогда же выстраиваться внутренняя структура дерева.
Для каждого элемента выстраивается полный путь в виде строки из серии
“2.5.7”, то есть предками элемента с id = 7 являются элементы с id = 2 и 5. Вот, как это происходит (Tree.php, строка 79).$this->tree[$child->id] = $this->tree[$item->id].'.'.$child->id;
$this->tree — массив путей, $item — очередной родительский элемент, $child — очередной дочерний элемент. Если путь элемента 8 был «3.8», то дочерний 11-й присоединяет к родительскому пути «.11».Все операции над деревом выполняются путём оперирования с массивом путей
$this->tree и элементов $this->items. Никаких дополнительных запросов к БД не требуется. Пример с поиском всех нижележащих элементов:/**Демонстрационный код я выложил на GitHub. Класс
* Возвращает нижележащие элементы для данного (дочерние и ниже)
*
* @param Item $item Целевой элемент
* @return array<Item>
*/
public function getDescendants(Item $item): array
{
$search = $this->getDescendantHashes($item);
$keys = array_keys($search);
$selectedItems = $this->getItemsByIds($keys);
return $selectedItems;
}
/**
* Ищет хэши нижележащих элементов
*
* @param Item $item
* @return array
*/
protected function getDescendantHashes(Item $item): array
{
$pattern = '/^'.$this->tree[$item->id].'\..+/s';
$search = preg_grep($pattern, $this->tree);
return $search;
}
Item подставлен исключительно для примера, класс Tree легко адаптируется для всех случаев, когда элемент может предоставить атрибуты id и parent.GitHub
tree/Tree.php at master · codewayblog/tree
Contribute to codewayblog/tree development by creating an account on GitHub.
👍2
Где и как читаются файлы конфигурации
Внутренние процессы фреймворка, как правило, скрыты от глаз — и это правильно, потому что задача фреймворка в том и состоит, чтобы избавить разработчика от возни с низкоуровневым кодом. Но иногда бывает полезно знать, где и как именно происходит тот или иной процесс. Сегодня разберёмся с чтением конфигурации в Laravel.
Получилось длинно, потому читайте здесь: https://telegra.ph/Gde-i-kak-chitayutsya-fajly-konfiguracii-04-10
Внутренние процессы фреймворка, как правило, скрыты от глаз — и это правильно, потому что задача фреймворка в том и состоит, чтобы избавить разработчика от возни с низкоуровневым кодом. Но иногда бывает полезно знать, где и как именно происходит тот или иной процесс. Сегодня разберёмся с чтением конфигурации в Laravel.
Получилось длинно, потому читайте здесь: https://telegra.ph/Gde-i-kak-chitayutsya-fajly-konfiguracii-04-10
Telegraph
Где и как читаются файлы конфигурации
Внутренние процессы фреймворка, как правило, скрыты от глаз — и это правильно, потому что задача фреймворка в том и состоит, чтобы избавить разработчика от возни с низкоуровневым кодом. Но иногда бывает полезно знать, где и как именно происходит тот или иной…
👍1
Отвязаться от конкретной инфраструктуры
Сложно себе представить веб-проект, который обходился бы только теми ресурсами, которые заложены в нём самом. Даже если вы не задействуете никаких внешних API, то хотя бы одна база данных у вас точно есть. И да, это тоже внешняя инфраструктура, потому что происходящее внутри БД вы не контролируете.
«Ну есть инфраструктура, и есть, в чём проблема?» — спросите вы и будете правы. Тут весь вопрос в том, как именно организовано обращение к ней. На начальном этапе разработки проекта часто в коде фигурирует обращение напрямую, используя классы и методы, специфичные для текущей версии инфраструктуры. Давайте возьмём ту же БД — пусть у вас MariaDB и она заведена как дефолтное соединение
1) Копипаста. Везде, где вам нужно получить эти данные, приведённую строчку придётся дублировать.
2) Если вы захотите получать предметы из другой БД, вам надо указать или поменять соединение (
3) Если же потребуется получать их совсем из другого источника (API, микросервис), то все такие места придётся полностью переписать.
4) Код, зависящий от инфраструктуры, труднее тестировать, потому что он не вполне предсказуем. Например, тесты могут поломаться из-за неполадок в конкретной физической БД.
Что можно с этим сделать?
Часть проблем в случае с БД решает ORM, абстрагирующая некоторые общие моменты — будь то соединения или типовые фильтры. Но это частичное и временное решение, для другой инфраструктуры оно и вовсе не подходит.
Для начала надо сформулировать, что именно для нас должен делать элемент инфраструктуры (назовём его сервисом). В нашем случае речь идёт об одном действии — «получить активные предметы», параметров нет, а возвращается коллекция. Каждое из действий станет методом будущего интерфейса. Интерфейс будет выглядеть примерно так.
Если же в вашем фреймворке/движке нет контейнера, то вся разница будет в том, что реализацию придётся создавать и передавать вручную, остальное работать будет так же.
Сложно себе представить веб-проект, который обходился бы только теми ресурсами, которые заложены в нём самом. Даже если вы не задействуете никаких внешних API, то хотя бы одна база данных у вас точно есть. И да, это тоже внешняя инфраструктура, потому что происходящее внутри БД вы не контролируете.
«Ну есть инфраструктура, и есть, в чём проблема?» — спросите вы и будете правы. Тут весь вопрос в том, как именно организовано обращение к ней. На начальном этапе разработки проекта часто в коде фигурирует обращение напрямую, используя классы и методы, специфичные для текущей версии инфраструктуры. Давайте возьмём ту же БД — пусть у вас MariaDB и она заведена как дефолтное соединение
mysql (напоминаю, в Laravel соединение с БД описывается в config/database.php). Для получения, допустим, ведущихся школьных предметов вы пишете в нужных местах примерно такой запрос.$subjects = DB::table('subjects')->where('active', 1)->get();
Возможные проблемы с этим:1) Копипаста. Везде, где вам нужно получить эти данные, приведённую строчку придётся дублировать.
2) Если вы захотите получать предметы из другой БД, вам надо указать или поменять соединение (
->connection()) во всех местах, где подобные запросы используются.3) Если же потребуется получать их совсем из другого источника (API, микросервис), то все такие места придётся полностью переписать.
4) Код, зависящий от инфраструктуры, труднее тестировать, потому что он не вполне предсказуем. Например, тесты могут поломаться из-за неполадок в конкретной физической БД.
Что можно с этим сделать?
Часть проблем в случае с БД решает ORM, абстрагирующая некоторые общие моменты — будь то соединения или типовые фильтры. Но это частичное и временное решение, для другой инфраструктуры оно и вовсе не подходит.
Для начала надо сформулировать, что именно для нас должен делать элемент инфраструктуры (назовём его сервисом). В нашем случае речь идёт об одном действии — «получить активные предметы», параметров нет, а возвращается коллекция. Каждое из действий станет методом будущего интерфейса. Интерфейс будет выглядеть примерно так.
<?phpКонкретный способ получения данных поместим в реализацию интерфейса.
namespace App\Contracts;
use Illuminate\Support\Collection;
interface SubjectsRepositoryContract
{
public function getActiveSubjects(): Collection
}
<?phpКак же нам теперь использовать новые классы в коде? Обращаться будем к интерфейсу.
namespace App\Repositories;
use App\Contracts\SubjectsRepositoryContract;
class DatabaseSubjectsRepository implements SubjectsRepositoryContract
{
public function getActiveSubjects(): Collection
{
return DB::table('subjects')→where('active', 1)→get();
}
}
<?phpОсталось настроить привязку в контейнере.
//controller
use App\Contracts\SubjectsRepositoryContract;
use Illuminate\View\View;
public function showSubjects(SubjectsRepositoryContract $subjectsRepository): View
{
$subjects = $subjectsRepository->getActiveSubjects();
return view('subjects.list', compact('subjects'));
}
<?phpВ контроллер будет подставляться нужная реализация за счёт внедрения зависимостей, и в случае необходимости мы можем её заменить — временно или постоянно. Во время тестирования можем подставить заглушку с фиксированными данными, например.
//AppServiceProvider.php
use App\Contracts\SubjectsRepositoryContract;
use App\Repositories\DatabaseSubjectsRepository;
...
App::bind(SubjectsRepositoryContract::class, DatabaseSubjectsRepository::class);
Если же в вашем фреймворке/движке нет контейнера, то вся разница будет в том, что реализацию придётся создавать и передавать вручную, остальное работать будет так же.
Итоги
Чтобы отвязаться от конкретного варианта сервиса, нужно:
1) формализовать выполняемые над ним операции;
2) создать на основе этого списка интерфейс;
3) переделать все обращения к сервису на обращения к соответствующим методам интерфейса;
4) детали работы с сервисом перенести в класс-реализацию интерфейса;
5) при наличии контейнера настроить в контейнере привязку реализации к интерфейсу, иначе создавать соответствующий класс и передавать в методы вручную.
Чтобы отвязаться от конкретного варианта сервиса, нужно:
1) формализовать выполняемые над ним операции;
2) создать на основе этого списка интерфейс;
3) переделать все обращения к сервису на обращения к соответствующим методам интерфейса;
4) детали работы с сервисом перенести в класс-реализацию интерфейса;
5) при наличии контейнера настроить в контейнере привязку реализации к интерфейсу, иначе создавать соответствующий класс и передавать в методы вручную.
Eloquent: за и против
Как и в любом серьёзном фреймворке, в Laravel есть встроенная ORM — она называется
https://telegra.ph/Eloquent-za-i-protiv-04-23
Как и в любом серьёзном фреймворке, в Laravel есть встроенная ORM — она называется
Eloquent. Предполагается, что разработчик будет использовать именно её. Но хороший специалист всегда знает границы применимости своих инструментов. Потому давайте разберёмся, какие за и против есть у этой ORM.https://telegra.ph/Eloquent-za-i-protiv-04-23
Telegraph
Eloquent: за и против
Как и в любом серьёзном фреймворке, в Laravel есть встроенная ORM — она называется Eloquent. Предполагается, что разработчик будет использовать именно её. Но хороший специалист всегда знает границы применимости своих инструментов. Потому давайте разберёмся…
Трудности при использовании массивов
Массив в PHP — тип крайне удобный для сколько-нибудь организованных данных: элементы можно пронумеровать или поименовать и затем пачкой обработать. В этом отношении он просто незаменим, и плюсы от использования массивов очевидны (например, лёгкий доступ к каждому элементу). Потому давайте поговорим о проблемах. В первую очередь они возникают, если массив собирается не в том же блоке кода, где затем используется, а где-то ранее (например, в другом классе).
Если говорить в целом, то массив, который собран не сейчас и/или не вами, представляет собой настоящий чёрный ящик, о котором толком ничего неизвестно, кроме того, что содержащиеся в нём элементы в принципе можно перебрать.
1. Неизвестны тип и последовательность ключей.
PHP позволяет делать ключи массива числовыми или строковыми (и смешивать эти типы), и расположение их может быть любым. Нет никакой гарантии, что в массиве существует тот или иной ключ, вам придётся каждый раз проверять это вручную.
2. Неизвестно даже приблизительно число элементов.
Более того, неясно, содержится ли в массиве хотя бы один элемент. Непустоту и количество вам также нужно проверять специально.
3. Неизвестны типы элементов (не говоря уже о значениях).
Каждый элемент нужно проверять, насколько он соответствует нужным нам критериям, и затем решать, что с ним делать, если он не подходит. Даже сравнительно дёшево узнать полный набор типов элементов без полного их перебора мы и то не можем.
4. Как следствие предыдущего, неизвестна вложенность.
Любой элемент массива может быть ещё одним массивом, и так далее до бесконечности. Обойти каждый элемент этого "дерева" — задача нетривиальная.
Что в итоге?
Я призываю вас подходить к использованию массивов с должной осторожностью и, где возможно, не применять их в чистом виде — например, применять классы-обёртки, которые обеспечивают большую предсказуемость организации данных. Как вариант — типизированные коллекции, в которые можно добавить только элементы одного типа. Или
Массив в PHP — тип крайне удобный для сколько-нибудь организованных данных: элементы можно пронумеровать или поименовать и затем пачкой обработать. В этом отношении он просто незаменим, и плюсы от использования массивов очевидны (например, лёгкий доступ к каждому элементу). Потому давайте поговорим о проблемах. В первую очередь они возникают, если массив собирается не в том же блоке кода, где затем используется, а где-то ранее (например, в другом классе).
Если говорить в целом, то массив, который собран не сейчас и/или не вами, представляет собой настоящий чёрный ящик, о котором толком ничего неизвестно, кроме того, что содержащиеся в нём элементы в принципе можно перебрать.
1. Неизвестны тип и последовательность ключей.
PHP позволяет делать ключи массива числовыми или строковыми (и смешивать эти типы), и расположение их может быть любым. Нет никакой гарантии, что в массиве существует тот или иной ключ, вам придётся каждый раз проверять это вручную.
2. Неизвестно даже приблизительно число элементов.
Более того, неясно, содержится ли в массиве хотя бы один элемент. Непустоту и количество вам также нужно проверять специально.
3. Неизвестны типы элементов (не говоря уже о значениях).
Каждый элемент нужно проверять, насколько он соответствует нужным нам критериям, и затем решать, что с ним делать, если он не подходит. Даже сравнительно дёшево узнать полный набор типов элементов без полного их перебора мы и то не можем.
4. Как следствие предыдущего, неизвестна вложенность.
Любой элемент массива может быть ещё одним массивом, и так далее до бесконечности. Обойти каждый элемент этого "дерева" — задача нетривиальная.
Что в итоге?
Я призываю вас подходить к использованию массивов с должной осторожностью и, где возможно, не применять их в чистом виде — например, применять классы-обёртки, которые обеспечивают большую предсказуемость организации данных. Как вариант — типизированные коллекции, в которые можно добавить только элементы одного типа. Или
ValueObject-ы с жёстко заданными свойствами и их типами (вместо массива с несколькими именованными значениями для передачи в другой модуль проекта).👍1
Создать свой composer-пакет: общий план
На фига козе баян Зачем вам ввязываться в это мероприятие, вы должны решить для себя сами. Я буду считать, что по какой-то причине вы уже решились взяться за пакет, но не знаете, что делать дальше. Что ж, в добрый путь. Ну и да, у меня есть уже свой пакет (https://packagist.org/packages/garmonic/laravel-features-arch), так что кое-что об этом процессе мне известно, поверьте.
https://telegra.ph/Sozdat-svoj-composer-paket-obshchij-plan-05-14
#пакеты #composer #php #laravel
https://telegra.ph/Sozdat-svoj-composer-paket-obshchij-plan-05-14
#пакеты #composer #php #laravel
packagist.org
garmonic/laravel-features-arch - Packagist
This package helps you to divide you Laravel project code by features, encapsulated in separate folders. Each feature has its own container, configuration, routes, views and providers. You can enable and disable your features in general config file.
👍1
Объединить нельзя дублировать
Итак, ситуация. У вас есть два проекта, на каждом из которых есть схожий функционал. Функционал этот задействует одно и то же хранилище данных, но всё остальное на проектах своё. Поступила задача, чтобы внутренняя часть этого функционала (бэкэнд) работала строго единым образом. Какие есть варианты архитектуры?
1. Привести код на одном из проектов к актуальным требованиям, на второй скопировать.
Плюсы:
- легко осуществить.
- работа проектов по-прежнему не зависит друг от друга.
Минусы:
- это дублирование кода со всеми вытекающими.
- если есть различия в изначальной архитектуре, такой вариант в принципе может быть невозможен (например, из-за разных версий языка и фреймворка).
2. Оставить актуальный код на одном проекте, со второго вызывать.
Плюсы:
- никакого дублирования.
Минусы:
- появляется зависимость вызывающего проекта от вызываемого. При проблемах со связью функционал перестанет работать.
3. Сохранить непосредственную отработку запроса на обоих проектах, а побочную (например, обработку события) на одном, со второго вызывать.
Плюсы:
- сохраняется известная независимость проектов.
Минусы:
- если побочная часть функционала так же важна для бизнес-логики, как и основная, то вариант аналогичен 2-му: частичная обработка запроса = отсутствие его обработки.
4. Вынести весь функционал на отдельный внешний сервис (API), с обоих проектов вызывать его.
Плюсы:
- отсутствие дублирования, подлинная единая ответственность;
- проекты по-прежнему независимы.
Минусы:
- логику кода придётся прилично переделать в обоих местах;
- даже если подходящий сервис уже есть, функционал нельзя будет перенести «как есть», адаптация понадобится. Если же API надо создавать с нуля, то этодополнительный геморрой свои дополнительные затраты;
- проблемы со связью никто не отменял — API точно так же может отвалиться, и тогда функционал не будет работать ни на одном из проектов.
Каково же решение?
На этот вопрос нет и не может быть единого ответа. Зависит от специфики функционала, конфигурации проектов, квалификации и занятости программистов, особенностей каналов связи — словом, от тысячи разных вещей. Каждая команда должна принять своё решение — поставить запятую (или две, или ни одной), выбрать какой-то из вариантов или вообще придумать свой. Специфика IT, ничего не поделаешь.
Итак, ситуация. У вас есть два проекта, на каждом из которых есть схожий функционал. Функционал этот задействует одно и то же хранилище данных, но всё остальное на проектах своё. Поступила задача, чтобы внутренняя часть этого функционала (бэкэнд) работала строго единым образом. Какие есть варианты архитектуры?
1. Привести код на одном из проектов к актуальным требованиям, на второй скопировать.
Плюсы:
- легко осуществить.
- работа проектов по-прежнему не зависит друг от друга.
Минусы:
- это дублирование кода со всеми вытекающими.
- если есть различия в изначальной архитектуре, такой вариант в принципе может быть невозможен (например, из-за разных версий языка и фреймворка).
2. Оставить актуальный код на одном проекте, со второго вызывать.
Плюсы:
- никакого дублирования.
Минусы:
- появляется зависимость вызывающего проекта от вызываемого. При проблемах со связью функционал перестанет работать.
3. Сохранить непосредственную отработку запроса на обоих проектах, а побочную (например, обработку события) на одном, со второго вызывать.
Плюсы:
- сохраняется известная независимость проектов.
Минусы:
- если побочная часть функционала так же важна для бизнес-логики, как и основная, то вариант аналогичен 2-му: частичная обработка запроса = отсутствие его обработки.
4. Вынести весь функционал на отдельный внешний сервис (API), с обоих проектов вызывать его.
Плюсы:
- отсутствие дублирования, подлинная единая ответственность;
- проекты по-прежнему независимы.
Минусы:
- логику кода придётся прилично переделать в обоих местах;
- даже если подходящий сервис уже есть, функционал нельзя будет перенести «как есть», адаптация понадобится. Если же API надо создавать с нуля, то это
- проблемы со связью никто не отменял — API точно так же может отвалиться, и тогда функционал не будет работать ни на одном из проектов.
Каково же решение?
На этот вопрос нет и не может быть единого ответа. Зависит от специфики функционала, конфигурации проектов, квалификации и занятости программистов, особенностей каналов связи — словом, от тысячи разных вещей. Каждая команда должна принять своё решение — поставить запятую (или две, или ни одной), выбрать какой-то из вариантов или вообще придумать свой. Специфика IT, ничего не поделаешь.
