Глупые ошибки бывают и в популярных фреймворках
На днях был случай — упала ошибка на одном из проектов с 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, ничего не поделаешь.
Что делать с исключением?
Чтобы не было недопонимания: я имею в виду экземпляры или наследников стандартного PHP-класса Exception, либо Throwable. Так что же с такими объектами делать?
Ответ зависит от того, в каком вы контексте и откуда объект исключения берётся.
1. Выбросить.
Подходит в том случае, если:
- нештатная ситуация произошла именно в вашем коде
- сделать вы с этим ничего не можете
- и ваш код при этом — не "последний рубеж обороны".
Тогда ваша святая обязанность написать что-то вроде
... но именно, что вроде. Лучше писать подробно, что случилось, да и класс исключения описать свой собственный.
Перейдём к вариантам, когда исключение генерируется не вами.
2. Обработать.
Вы ожидаете, что в вызываемом вами коде что-то может поломаться, и пишете, например,
а) Если проблема не криминальная (не удалось разобрать необязательный параметр, например), то можно обойтись логированием ошибки, и продолжить выполнение.
б) В противном случае, если при этом именно вы должны отдать пользователю ответ, то его нужно сформировать немедленно.
3. Завернуть в своё исключение.
Как правило, вызывающий ваш класс код будет ждать от вас исключения определённого типа и перехватывать именно их. Поэтому есть смысл ловить исключение и вместо него выбросить своё, либо с тем же текстом, либо с прикреплением предыдущего в поле previous.
На каком-то этапе нам может быть более важна консистентность данных по сравнению с пользовательским опытом (хотя это и не есть хорошо). В таком случае исключение сознательно можно не обрабатывать, с тем, чтобы была выброшена 500-я ошибка. Но призываю вас к внимательности. Перед использованием указанного способа работы с исключениями убедитесь, что именно такого поведения вы и добиваетесь. И уж во всяком случае стоит написать про это комментарий в коде.
Чтобы не было недопонимания: я имею в виду экземпляры или наследников стандартного PHP-класса Exception, либо Throwable. Так что же с такими объектами делать?
Ответ зависит от того, в каком вы контексте и откуда объект исключения берётся.
1. Выбросить.
Подходит в том случае, если:
- нештатная ситуация произошла именно в вашем коде
- сделать вы с этим ничего не можете
- и ваш код при этом — не "последний рубеж обороны".
Тогда ваша святая обязанность написать что-то вроде
throw new Exception('Что-то пошло не так...')
... но именно, что вроде. Лучше писать подробно, что случилось, да и класс исключения описать свой собственный.
Перейдём к вариантам, когда исключение генерируется не вами.
2. Обработать.
Вы ожидаете, что в вызываемом вами коде что-то может поломаться, и пишете, например,
try {
// основная логика
} catch (LogicException $exception) {
// обработка исключения
}
Что писать в обработку, зависит от критичности ошибки в смысле бизнес-логики. а) Если проблема не криминальная (не удалось разобрать необязательный параметр, например), то можно обойтись логированием ошибки, и продолжить выполнение.
б) В противном случае, если при этом именно вы должны отдать пользователю ответ, то его нужно сформировать немедленно.
try {
// основная логика
} catch (LogicException $exception) {
return response()->json(['success'=>false, 'message'=>'Переданные данные ошибочны'], 400);
}
3. Завернуть в своё исключение.
Как правило, вызывающий ваш класс код будет ждать от вас исключения определённого типа и перехватывать именно их. Поэтому есть смысл ловить исключение и вместо него выбросить своё, либо с тем же текстом, либо с прикреплением предыдущего в поле previous.
try {
// основная логика
} catch (LogicException $exception) {
throw new CustomException($exception->getMessage()); // вариант 1
//или
throw new CustomException('Текст ошибки', 0, $exception); // вариант 2
}
4. Проигнорировать.На каком-то этапе нам может быть более важна консистентность данных по сравнению с пользовательским опытом (хотя это и не есть хорошо). В таком случае исключение сознательно можно не обрабатывать, с тем, чтобы была выброшена 500-я ошибка. Но призываю вас к внимательности. Перед использованием указанного способа работы с исключениями убедитесь, что именно такого поведения вы и добиваетесь. И уж во всяком случае стоит написать про это комментарий в коде.
// Может быть выброшено исключение, не нужно обрабатывать, пусть валится в 500-юТак что смотрите по ситуации и выбирайте тот вариант, который для неё наиболее уместен.
$result = someAction();
👍2
Когда и как делить источник данных на два
Веб-проект растёт, становится успешным, на него увеличивается нагрузка. Наступает момент, когда наличие источника данных в единственном экземпляре становится проблематичным, и его приходится делить. Как определить этот момент и что делать в таком случае, расскажу далее.
Как возникает проблема
На этапе проектирования и начальной стадии существования проекта одного хранилища данных каждого типа вполне достаточно: скажем одна MariaDB-база, инстанс Redis для кэша и файловое хранилище типа S3. Ничего лишнего, удобно и понятно, как с этим всем работать. Но, если вы всё правильно делаете и пользователи довольны вашим сервисом, их число постепенно растёт. Запросов к вашим ресурсам в секунду становится всё больше — например, начинали с 5, потом стало 10, 15, 20, 30, наконец, 50. Через день заглядывает хмурый сисадмин. Он бурчит, что сервак не справляется, и надо с этим что-то делать. И с каждым разом его голос звучит всё более мрачно.
Решение на уровне инфраструктуры
Что ж, если вы узнаёте описанную ситуацию, то вопрос разделения явно назрел. Все ресурсы, которые перегружены, нужно делить на несколько и распределить нагрузку между ними. Простейший и минимальный вариант — разграничить запись и чтение. В зависимости от специфики вашего проекта более частой может быть любая из этих операций. Обычно это чтение данных. Для этой операции выделяется более мощный сервер или несколько, а менее мощный остаётся для другой операции. В случае, когда серверов несколько, потребуется также настроить репликацию с основного источника на дополнительные, чтобы набор данных полностью совпадал.
Адаптация кода
По поводу инфраструктуры разобрались, а как же код? Ведь он был написан с расчётом на один источник данных. Его придётся адаптировать под новые реалии. Если обращения к источнику данных собраны в ряде компактных классов (типа *Repository), то доделать их будет легко — указать в них другое соединение или файловую систему. Возможно, просто поменять где-то привязки в контейнере. Иначе придётся перерывать весь проект, чтобы провести такие изменения. Но так или иначе должно быть введено правило: читать из источника "для чтения", писать в источник "для записи".
Что с архитектурой?
С точки зрения архитектуры полезным будет применение подхода CQRS, в основе которого — принципиальное разделение потоков получения данных и изменения данных. Для этого создаются разные наборы классов со своей логикой. Возможно, про CQRS я напишу ещё отдельный пост, а пока можно прочитать, к примеру, эту статью.
Итоги
1. Делить хранилище данных надо тогда, когда нагрузка на него потенциально приближается к критической (ну или несколько раньше).
2. В первую очередь стоит разграничить запись и чтение, и распределить ресурсы соответствующих источников в зависимости от нагрузки на соответствующий поток.
3. После изменения инфраструктуры потребуется адаптация кода по правилу "читать из источника на чтение, писать в источник на запись".
4. При решении архитектурного вопроса в связи с адаптацией стоит рассмотреть вариант с CQRS — насколько он применим и реализуем в вашем случае.
Веб-проект растёт, становится успешным, на него увеличивается нагрузка. Наступает момент, когда наличие источника данных в единственном экземпляре становится проблематичным, и его приходится делить. Как определить этот момент и что делать в таком случае, расскажу далее.
Как возникает проблема
На этапе проектирования и начальной стадии существования проекта одного хранилища данных каждого типа вполне достаточно: скажем одна MariaDB-база, инстанс Redis для кэша и файловое хранилище типа S3. Ничего лишнего, удобно и понятно, как с этим всем работать. Но, если вы всё правильно делаете и пользователи довольны вашим сервисом, их число постепенно растёт. Запросов к вашим ресурсам в секунду становится всё больше — например, начинали с 5, потом стало 10, 15, 20, 30, наконец, 50. Через день заглядывает хмурый сисадмин. Он бурчит, что сервак не справляется, и надо с этим что-то делать. И с каждым разом его голос звучит всё более мрачно.
Решение на уровне инфраструктуры
Что ж, если вы узнаёте описанную ситуацию, то вопрос разделения явно назрел. Все ресурсы, которые перегружены, нужно делить на несколько и распределить нагрузку между ними. Простейший и минимальный вариант — разграничить запись и чтение. В зависимости от специфики вашего проекта более частой может быть любая из этих операций. Обычно это чтение данных. Для этой операции выделяется более мощный сервер или несколько, а менее мощный остаётся для другой операции. В случае, когда серверов несколько, потребуется также настроить репликацию с основного источника на дополнительные, чтобы набор данных полностью совпадал.
Адаптация кода
По поводу инфраструктуры разобрались, а как же код? Ведь он был написан с расчётом на один источник данных. Его придётся адаптировать под новые реалии. Если обращения к источнику данных собраны в ряде компактных классов (типа *Repository), то доделать их будет легко — указать в них другое соединение или файловую систему. Возможно, просто поменять где-то привязки в контейнере. Иначе придётся перерывать весь проект, чтобы провести такие изменения. Но так или иначе должно быть введено правило: читать из источника "для чтения", писать в источник "для записи".
Что с архитектурой?
С точки зрения архитектуры полезным будет применение подхода CQRS, в основе которого — принципиальное разделение потоков получения данных и изменения данных. Для этого создаются разные наборы классов со своей логикой. Возможно, про CQRS я напишу ещё отдельный пост, а пока можно прочитать, к примеру, эту статью.
Итоги
1. Делить хранилище данных надо тогда, когда нагрузка на него потенциально приближается к критической (ну или несколько раньше).
2. В первую очередь стоит разграничить запись и чтение, и распределить ресурсы соответствующих источников в зависимости от нагрузки на соответствующий поток.
3. После изменения инфраструктуры потребуется адаптация кода по правилу "читать из источника на чтение, писать в источник на запись".
4. При решении архитектурного вопроса в связи с адаптацией стоит рассмотреть вариант с CQRS — насколько он применим и реализуем в вашем случае.
Хабр
Основы CQRS
Данная статья основана на материале из различных статей по CQRS, а также проектов, где применялся такой подход. Системы управления предприятиями, проектами, сот...
👍1
Когда пригодятся типы-перечисления
Вопрос стал особенно актуальным в связи с появлением таких типов в PHP 8.1. Теперь они нативно поддерживаются языком.
Короткий ответ: перечисления применимы везде, где значение переменной должно быть выбрано из закрытого (и достаточно ограниченного) списка.
Вот пример перечисления для цветов светофора. Кроме как "красный" (
В разработке то и дело приходится иметь дело с такими специфическими данными, типа статусов. Каково может быть состояние письма, например? "Подготовлено", "Отправлено", "Не удалось отправить". Четвёртого не дано. Или, как вариант, источники данных для приходящих картинок. Вряд ли их будет даже десять. И вот что со всем этим делать?
Глобальных проблем тут две: валидация и понимание происходящего.
По поводу валидации. Если мы храним значения перечислимого типа как строки или числа, мы вынуждены каждое приходящее в соответствующую переменную значение проверять — укладывается ли оно в наше перечисление? А что, если просто случайно совпало, а строка/число означает совсем другое? Обычная проблема использования скалярных типов не по назначению. В таких случаях переходят к ValueObject-ам (в которых и пишется валидация), но Enum-ы куда удобнее для работы с перечислениями.
Что же касается понимания, то трудности здесь схожие. Мы не можем круглосуточно помнить, что эта строка (или число) соответствуют элементам ограниченного множества, и в один прекрасный момент туда обязательно попадёт что-то не то — хотя бы из-за опечатки.
Вместо вывода: если вы уже перешли на PHP 8.1 и работаете с подобными значениями из ограниченного списка, присмотритесь к
Вопрос стал особенно актуальным в связи с появлением таких типов в PHP 8.1. Теперь они нативно поддерживаются языком.
Короткий ответ: перечисления применимы везде, где значение переменной должно быть выбрано из закрытого (и достаточно ограниченного) списка.
Вот пример перечисления для цветов светофора. Кроме как "красный" (
TrafficLightColor::Red), "жёлтый" (TrafficLightColor::Yellow), "зелёный" (TrafficLightColor::Green), ничего выбрать невозможно. Это может быть очень удобно.<?phpТеперь поясню подробнее.
enum TrafficLightColor
{
case Red;
case Yellow;
case Green;
}
?>
В разработке то и дело приходится иметь дело с такими специфическими данными, типа статусов. Каково может быть состояние письма, например? "Подготовлено", "Отправлено", "Не удалось отправить". Четвёртого не дано. Или, как вариант, источники данных для приходящих картинок. Вряд ли их будет даже десять. И вот что со всем этим делать?
Глобальных проблем тут две: валидация и понимание происходящего.
По поводу валидации. Если мы храним значения перечислимого типа как строки или числа, мы вынуждены каждое приходящее в соответствующую переменную значение проверять — укладывается ли оно в наше перечисление? А что, если просто случайно совпало, а строка/число означает совсем другое? Обычная проблема использования скалярных типов не по назначению. В таких случаях переходят к ValueObject-ам (в которых и пишется валидация), но Enum-ы куда удобнее для работы с перечислениями.
Что же касается понимания, то трудности здесь схожие. Мы не можем круглосуточно помнить, что эта строка (или число) соответствуют элементам ограниченного множества, и в один прекрасный момент туда обязательно попадёт что-то не то — хотя бы из-за опечатки.
<?phpИ вот, почта уходит, а сообщение говорит об обратном. Были бы эти статусы вариантами
const MAIL_STATUSES = ['prepared', 'sent', 'sending_failed'];
//...
$result = Mail::send($letter);
if($result->status == 'send') { // Всего одна буква разницы, а условие уже работает неправильно
return response()->json(['message'=>'Письмо отправлено']);
} else {
return response()->json(['message'=>'Ошибки при отправке']);
}
Enum, то даже редактор уже ткнул бы разработчика в ошибку, указав, что такого варианта нет.Вместо вывода: если вы уже перешли на PHP 8.1 и работаете с подобными значениями из ограниченного списка, присмотритесь к
Enum.
