Beer::PHP 🍺
2K subscribers
12 photos
2 videos
96 links
Тут публікуються короткі замітки про PHP, Linux, Unit Testing, DB, OOP тощо, витяги зі статей, книг, відео, курсів та інших матеріалів.

Тепер тобі більше не потрібно перегортати тонни інформації ;)

@genkovich — написати автору каналу.
Download Telegram
Использование индексов в MySQL

Чем больше мы пользуемся ORM, тем меньше задумываемся об оптимизации БД, до тех пор пока не прижмёт. В простых кейсах для ускорения запроса проблем не возникает, но если случай чуть сложнее чем "добавить индекс", то разработчики часто не знают за что хвататься. Здесь хочу оставить пару заметок, которые могут натолкнуть на различные решения в подобной ситуации.

👉 Все мы знаем, что индексы используются для быстрого поиска строк с определенными значениями столбцов. Без индекса MySQL будет начинать поиск с первой строки, а затем читать всю таблицу. Чем больше таблица, тем дороже эта операция.

На что обратить внимание при оптимизации?

• Не исключен случай когда одна колонка используется в нескольких индексах. В таком случае MySQL выбирает индекс который вернет наименьшее кол-во строк (наиболее избирательный).

• При использовании составного (композитного) индекса помните, что он может использоваться и в более простых выборках, но только по столбцам перечисленным слева направо. Например индекс (col1, col2, col3) будет работать для выборок (col1), (col1, col2), и (col1, col2, col3), но не будет для (col2, col3) или (col3).

🤝 Для получения строк из других таблиц при JOIN

• Для сравнения строковых столбцов оба столбца должны использовать одну и ту же кодировку. Например, сравнение столбца utf8 со столбцом latin1 исключает использование индекса.

• MySQL может использовать индексы более эффективно, если они одного и того же типа и размера. В этом контексте VARCHAR и CHAR считаются одинаковыми, если они объявлены с одинаковым размером. Например, VARCHAR (10) и CHAR (10) имеют одинаковый размер, а VARCHAR (10) и CHAR (15) - нет.
Сравнение столбцов разного типа (например, VARCHAR с DATETIME или INT) может препятствовать использованию индексов, если при этом необходимо преобразование. Например в одной таблице у вас INT 1, а в другой VARCHAR ' 1' или '00001'.

🙈 Не слишком очевидное

• Индексы менее важны для маленьких таблиц или для больших, из которых нам нужно извлечь все данные или большую их часть. В таком случае последовательное чтение выполняется быстрее, чем при работе с индексом. Всё потому, что последовательное чтение минимизируют поиск на диске, даже если нам нужны не абсолютно все строки.

• Оптимизатору можно задать подсказку по выбору или игнорированию индекса.

SELECT * FROM table1 USE INDEX (col1_index,col2_index)
WHERE col1=1 AND col2=2 AND col3=3;

SELECT * FROM table1 IGNORE INDEX (col3_index)
WHERE col1=1 AND col2=2 AND col3=3;

👍 Ставь 🍺 если было полезно и если хочешь еще заметок по этой теме.

#MySQL #junior #source
Статические анонимные функции 😱

Буквально на днях пришел вопрос от одного из подписчиков касательно контента этого поста. Звучал он так: "А зачем делать callback’и в функции сортировки (usort), статическими?" И я подумал, что это действительно хороший вопрос, на который стоит обратить внимание.

📄 Из документации:
При объявлении в контексте класса, текущий класс будет автоматически связан с ним, делая $this доступным внутри функций класса. Если вы не хотите автоматического связывания с текущим классом, используйте статические анонимные функции.

Выходит, что когда closure объявляется в контексте класса, то класс автоматически привязывается к замыканию. Это означает, что $this доступен внутри области анонимной функции. Вот такой вот пример.

❗️На первый взгляд "да и чёрт с ним", но стоит копнуть чуть глубже.

Замыкание, содержащее ссылку на $this, может предотвратить сборку мусора для этого объекта, что, в свою очередь, может существенно повлиять на производительность. Вот примеры с использованием static и без него. Ну и gist, чтобы самостоятельно поиграться.

👉 Если подвести короткий итог, то анонимные функции без static стоит использовать если вам необходимо привязать объект к области видимости выполнения функции. Во всех остальных случаях можно использовать static, как минимум, чтобы случайно не выстрелить себе в ногу.

#php #middle #memory #source
Whole value concept (Quantity pattern)

Я часто вижу, что этому концепту уделяют мало внимания при проектировании Value Objects, потому решил отдельно на нём остановиться.

❗️Следует создавать и использовать объекты, которые имеют значение в рамках вашего бизнеса.

👉 Идея простая. Представим, что у нас есть геопозиция. Чтобы понять где именно находится точка нам нужна и широта и долгота. Поскольку сами по себе "широта" или "долгота" не имеют смысла друг без друга, значит они должны находиться в одном месте, внутри одного объекта. Другими словами не нужно создавать отдельные VO, если сами по себе они ничего не значат, а только являются составляющей другого объекта.

👉 Другой пример. У нас есть сумма денег, которую нам нужно сложить с другой суммой. Чтобы принять решение можем ли мы сложить две amount, мы должны проверить currency. Поскольку currency напрямую влияет на логику вычислений, то оно должно находиться там-же, где и amount.

Это может быть что угодно, такие штуки как валюта, координаты, календарный период, номер телефона, расстояние, вес и т.д.

📌 То есть если у нас есть данные которые влияют на логику - они должны быть частью состояния объекта где эта логика реализована. Да-да, вычисления(логика) также должны находиться внутри (например сложение/вычитание денег или вычисление расстояния в случае с гео).

📌 Если же в объекте хранятся данные которые на логику реализованную в этом объекте никак не влияют - было бы неплохо эти данные оттуда вынести что бы не мешали.

Это не значит, что нужно совсем перестать оборачивать примитивные типы (строки, числа и т.д.). Это значит, что при проектировании стоит задумываться о целесообразности того или иного объекта в вашей предметной области.

#php #oop #middle #source
Правила по проектированию Entity (part 1)

🗒 В предыдущих статьях я уже несколько раз ссылался на понятие сущности Entity. Разные авторы (напр. Эванс, Нобак, Вернон) дают немного разные определения этого понятия, но суть сильно от этого не меняется.
Сущности — это объекты, которые хранят состояние вашего приложения. Но не "просто хранят".
И вот несколько правил, которые помогут вам проектировать такие объекты:

📌 Сущность всегда должна защищать свои доменные инварианты и следить за тем, чтобы она находилась в согласованном состоянии. Она не должна существовать в вашем приложении если внутри неполные или невалидные данные.

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

📌 Когда обновление определенного поля фактически представляет действие, выполняемое над объектом, определите для него метод в самой сущности. Задача такого метода также заключается в проверке предоставленных ему данных, он должен убедиться, что можно обновить данные, учитывая текущее состояние объекта.

Например вы работаете с заказами. Заказ товара может быть отменен, если он не доставлен. Вместо того чтобы где-то во вне сущности делать:

$order->getStatus(), 
// isn't delivered
$order->setCancel()

Определите метод cancel(), который будет выполнять проверки внутри сущности и если всё согласовано — менять её состояние.

📌 Не думайте всё время о базе данных.
Стоит оговориться, что речь идёт именно о проектировании структуры и самой сущности. Конечно, есть много случаев, когда нам нужно считаться с нашей базой (высокая конкуренция запросов, deadlocks и т.д.), но идея в том, чтобы перестроить мышление и думать об объектах, как о вещах в реальном мире, а не как о данных в таблице. Просто примите для себя, что маппинг данных в БД это отдельная задача.

👉 Еще несколько правил, касающихся ID, построения связей с другими сущностями, фишки с использованием репозиториев будут во второй части.

#php #oop #junior #entity #source
Использование индексов в MySQL (part 2)

Вот еще несколько важных заметок, которые являются дополнением этого поста.

📌 Повторяющиеся индексы могут не замедлить запросы SELECT, но вполне могут замедлить запросы на INSERT (а в некоторых случаях и UPDATE). В целом рекомендуется избегать дублирования ключей. Например если в одной таблице 2 индекса:

KEY `firstname` (`firstname`),
KEY `firstname_lastname_id` (`firstname`,`lastname`,`id`)

то firstname является дубликатом firstname_lastname_id, так как firstname является первым столбцом индекса firstname_lastname_id.

👉 Вот тулза, которая может вам помочь найти подобные случаи и решить, как с ними поступать.

📌 Обратная ситуация, неиспользуемые индексы также следует удалять т.к. это дополнительные расходы памяти и времени на вставку и апдейт. Для этого есть еще одна тулза, которая может вам помочь найти подобные кейсы, но после нахождения обязательно перепроверьте вручную, чтобы не удалить лишнего.

📌 Начиная с версии 8+, MySQL поддерживает индексы по убыванию (нисходящие, DESC), что означает, что он может хранить индексы в порядке убывания. Это может пригодиться, когда у вас есть выборки где надо получать последние добавленные данные.

CREATE TABLE t (
c1 INT, c2 INT,
INDEX idx1 (c1 ASC, c2 ASC),
INDEX idx2 (c1 ASC, c2 DESC),
INDEX idx3 (c1 DESC, c2 ASC),
INDEX idx4 (c1 DESC, c2 DESC)
);

📌 Также у вас могут быть таблицы с данными, которые не нужны вам в выборке или вообще нужны редко. Подумайте о том, чтобы разделить такую таблицу (логически или по необходимости использования данных). Это также ускорит выборку и снизит потребление CPU.

👍 Если вернуться к первым двум пунктам этой заметки, то следует упомянуть, что у Percona Toolkit очень большой набор инструментов, с которым рекомендую как минимум ознакомиться ;) Если и этот пост наберет много 🍺, то напишу короткую заметку о самых интересных на мой взгляд.

#MySQL #junior #source
Tell, Don't Ask (TDA)

Еще в 1997 году А. Шарп сформулировал такой принцип:

📃 Процедурный код сначала получает информацию, а затем принимает решения. Объектно-ориентированный код говорит объектам, что нужно делать.

То, что мы принимаем решения за пределами объектов — нарушает их information hiding. В этом примере мы скрыли детали реализации. Это упростило понимание и поддержку нашего кода, а также сделало наш объект более осознанным.

❗️ Внимание, обычно в этом месте кто-то вспоминает про "анемичную модель", а кто-то про "god objects" и начинается дикий срач, так что будьте готовы, если хотите с кем-то обсудить эту тему ;)

👉 Принципы — это не истина последней инстанции. Безусловно, мы должны держать TDA в голове, но не слепо ему следовать. Да он может привести к:

🔹 Раздутию классов и увеличения их сложности;
🔹 Увеличению coupling из-за того что у объекта много ответственности;
🔹 Нарушению SRP (single responsibility principle) в конце концов;

С другой стороны, если для регистрации User вам нужны только email и password, которые совсем не принимают участия в других процессах, то действительно ли всё должно быть в одном объекте? Или это совсем другой объект напр. Credentials?

Конечно многое зависит от задачи и контекста, но в любом случае стоит придерживаться здравого смысла.

#oop #middle #source
Regular Expression

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

👉 Выберите удобный разделитель (delimiter)

Речь идет о символах, в которые мы оборачиваем наше выражение. Чаще всего в примерах используется slash "/"

/(foo|bar)/i

❗️На самом деле для разделителя можно выбрать любой символ, например ~,!, @, #, $. Разделителями НЕ могут быть буквенно-цифровые символы (AZ, az и 0-9), многобайтовые символы (Такие как Emojis 😢) и обратная косая черта (\).

Но зачем выбирать другой разделитель? Всё дело в том, что все вхождения символа разделителя в выражение должны быть экранированы, потому на выходе мы получаем:

preg_match('/^https:\/\/example.com\/path/i', $uri);
// или
preg_match('~^https://example.com/path~i', $uri);

👉 Именуйте скобочные группы (группы захвата)

Скобочными группами называется то, что находится внутри () скобок. Найденные результаты будут переданы в $matches.

$pattern = '~Price: (£|€)(\d+)~';
// или
$pattern = '~Price: (?<currency>£|€)(?<price>\d+)~';

Именуя скобочные группы, мы сразу получаем два преимущества: это упростит чтение регулярного выражения, а также имена будут отображены в $matches;

👉 Используйте символьные классы

Это также поможет сделать ваше выражение более читабельным. Самый распространенный класс \d - представляет собой одну цифру (эквивалентно [0-9]), при этом \D (в верхнем регистре) эквивалентно [^0-9] то есть не является цифрой. То есть использование верхнего регистра является обратным основному выражению.

\w соответствует слову (может состоять из букв, цифр и подчёркивания) A-Za-z0-9_
\s соответствует символу пробела (включая табуляцию и прерывание строки)
. соответствует любому символу

👌 Так-же следует упомянуть, что при использовании выражений с поддержкой unicode (флаг /u), вы можете использовать еще огромное количество символьных классов \p{}. Например:

\p{Sc} — любой знак валюты
\p{P} — любой знак пунктуации
\p{L} — любая буква из любого языка

Весь перечень можно посмотреть здесь.

#junior #regexp #source
👍1
The Everybody Poops Rule (Все какают)

👉 Как мне кажется отличная аллегория, которая применима к программированию. Все мы постоянно сталкиваемся с ситуациями, когда в коде приходится идти на компромиссы. Иногда из-за нехватки времени, иногда для избежания оверинжиниринга, иногда из-за несовершенства системы, нехватки знаний и т.д. И мы сознательно оставляем в коде "дерьмо", ведь не каждая часть нашего приложения должна быть идеальной.

📌 Все какают. Но дома это делают не в каждой комнате. У вас есть специальная комната, в которой вы какаете, ставите дверь, и только там это делаете.

Да, звучит странно, но зато хорошо запоминается 😂

❗️ Важно понимать, что этот процесс управляемый. Данное правило как раз помогает с этим жить при помощи сокрытия и инкапсуляции. Если код изолирован — не важно, что скрывается за интерфейсом. Главное, что мы гадим в определенных частях и "держим дверь закрытой".

👍 Конечно, у подобных мест тоже следует определить стандарты. То есть нельзя совсем забить и втулить туда какой-то God Object с методами в 1000 строк, вызывающие запросы к БД в foreach. Здесь важно найти баланс. Это тоже непростая работа. Помните, положив код "за дверь" он не становится лучше, а значит учитывайте риски того, что вам придётся его переписывать.

❗️ Также то, что вы можете всё изолировать — не значит, что это нужно делать всегда и везде. Прежде чем это сделать задайте себе несколько вопросов:

• Как часто код будет использоваться?
• Как долго его можно не трогать?
• Является ли это основным доменом в вашей компании? (здесь лучше этого не делать)

👍 Еще круче — сразу планировать возможный рефакторинг. То, что сегодня вы изолировали может понадобиться бизнесу в будущем или где-то аукнуться в самый неподходящий момент. Круто быть к этому готовым. Лучше всего это место задокументировать, чтобы точно знать о всех отклонениях.

#junior #oop #source
👍1
Data Transfer Object (DTO) и как его готовить?

В предыдущих постах я уже упоминал что это, но хочу остановиться на нём немного подробнее, чтобы закрыть часто задаваемые вопросы.

Зачем он нужен?

Для упрощения рассмотрим всё тот-же кейс передачи данных между слоями внутри приложения.

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

Для примера рассмотрим контроллер и консольную команду внутри которых мы пытаемся вызывать уже существующий сервис (или command handler) для смены адреса доставки. И Request и Input являются частью инфраструктуры фреймворка, на которую мы не можем повлиять, при этом они имеют разный интерфейс. Плюс ко всему это делает нашу бизнес логику зависимой от той самой инфраструктуры.

Как раз здесь нам на помощь и приходит DTO, который позволит отделить слои друг от друга.

Должен ли DTO содержать валидацию?

👉 Нет.


Внутри вообще ничего не должно быть, кроме примитивных данных. Оставьте первый этап валидации вашему фронтенду (или валидатору фреймворка), а второй уже самому доменному слою (Value Object, Entity и т.д.). То есть DTO не должен выбрасывать никаких исключений. Всё что вам нужно, это привести данные к правильным типам, присвоить полям их значения или null (если в вашем случае это допустимо). Это оставит знания о том, как работать с объектами домена внутри ядра приложения, а не в коде инфраструктуры.

❗️ Называйте ваши DTO по их намерениям (действиям)

👍 Если данные будут использоваться для изменения адреса доставки заказа (как в примерах выше), назовите его ChangeDeliveryAddress (а не DeliveryAddressDTO). Во первых это уменьшит путаницу, т.к. разные действия, чаще всего будут иметь разный набор данных. Например DTO для создания адреса доставки может не содержать ID, а для изменения он уже обязателен.

#junior #php #dto #source
Validation (part 1). Валидация внутри доменного слоя приложения

Уже несколько раз меня просили написать заметку по этой теме и вот, после нескольких подготовительных постов, на которые я буду ссылаться наконец можно приступить. Чуть позже мы рассмотрим и пользовательскую валидацию и поговорим про ограничения в базе данных, но начнем мы сразу с доменного слоя нашего приложения, то есть с той самой бизнес логики.

📌 Валидация сущности (Entity).

Рано или поздно, пользовательские данные переданные в наше приложение попадают во внутрь Entity. В одном из предыдущих постов, я уже писал, что сущность сама должна защищать свои инварианты, хранить в себе только валидные данные и при этом валидировать данные самостоятельно.

Не нужно создавать для Entity сервисы валидации. Вам придется делать бесконечные и ненужные геттеры внутри Entity, следить за тем что нужно обновить сервис в случае изменения самой сущности и не забывать его вызвать каждый раз при её создании.

👍 Конструктор должен принимать все обязательные для существования сущности параметры и валидировать их перед тем, как присвоить значение свойству. Все необязательные параметры могут быть заданы значениями по-умолчанию и/или быть присвоенными отдельными методами, в которых также следует добавлять проверки перед присваиванием. В случае если нас что-то не устраивает — кидаем Exception. Пример

Но ведь мы же не будем показывать пользователям исключения?

Всё правильно, исключения не для пользователей. Exceptions, трассировка и контекст должны быть видны только разработчикам. Все исключения выброшенные разработчиком должны быть обработаны перед тем как вывести пользователю что-то на экран. Но об этом чуть позже.

📌 Используйте Value Objects для проверки отдельных значений.

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

👍 Из предыдущего примера мы можем отдельно вынести AccountNumber, переместив в него всю валидацию, отдельно выделить Value Object Money, который также может взять на себя операцию сложения для логики пополнения счета. Тогда наша Entity будет иметь примерно следующий вид. Так как в основной сущности мы уже работаем с валидными Value Objects, то нет необходимости проверять что-то дополнительно внутри сущности, мы и так всё затайпхинтили.

👉 Тема очень обширная, так что ставь 🍺 если интересна информация про валидацию пользовательских данных, взаимодействие с БД, а также про Incomplete, Invalid и Inconsistent объекты.

#php #oop #junior #source
Бинарный поиск и "О-большое"

К сожалению, в последнее время встречаю всё больше людей, которые совсем не знакомы с темой алгоритмов. Аргументируют это тем, что "да зачем мне это нужно, если я пилю крадики" и они правы. Разница только в том, что с таким подходом далеко не уедешь и велика вероятность так и пилить крадики до конца своей карьеры. Лично я считаю, что действительно не стоит сразу слишком глубоко копать в эту тему, но базовые принципы знать обязательно. Как минимум базовые понятия встречаются во многих книгах, статьях и видео. И чтобы правильно понять, что до вас хочет донести автор — нужно чуть-чуть разобраться.

Представьте, что ваш друг загадал число, от 1 до 100, а вам нужно его отгадать. При каждой попытке друг будет давать вам один из трёх ответов "Мало" , "Много", "В точку!". Если перебирать все варианты подряд (1, 2, 3, 4... то есть прямым поиском), то вы рискуете использовать 100 попыток, при самом плохом случае.

👌 Но что если вы сразу ударите в середину и назовете число 50? "Мало", и вы сразу отсекли половину вариантов. Затем "75" — "Много", и еще половина вариантов ушла. Именно так и работает бинарный поиск.

❗️Важно, что бинарный поиск работает только в том случае, если список отсортирован.

📌 Время выполнения и "О-большое"

💁‍♂️ Возможно вы забыли что такое логарифм, но точно помните, что такое возведение в степень. Так вот, запись log(2) 8 означает, в какую степень нужно возвести 2, чтобы получить 8, итак log(2) 8 = 3.

"О-большое" описывает, насколько быстро работает алгоритм. Простой поиск должен проверить каждый элемент. Для списка из 4 миллиардов (или любое другое n) чисел потребуется до 4 миллиар­дов попыток. Таким образом, максимальное количество попыток совпадает с размером списка. Такое время выполнения называется линейным и обозначается O(n).

С бинарным поиском дело обстоит иначе. Для списка из 4 миллиардов элементов, потребуется не более более 32 попыток. Впечатляет, да? Бинарный поиск выполняется за логарифмическое время и его сложность описывается как O(log n).

Если это время, то где же секунды?

А их здесь нет. "О-большое" не сообщает время в секундах, а позволяет сравнить количе­ство операций. Оно указывает, насколько быстро возрастает время выполнения алгоритма. А время в секундах уже будет зависеть от размера исходных данных, вычислительных мощностей и т.д.

❗️ "О-большое" определяет время выполнения в худшем случае.

То
есть если ваш друг, загадал число "1", то при прямом поиске вы угадаете его моментально, так как оно стоит на первом месте O(1). Но простой поиск всё равно выполняется за время O(n), фактически это утверждение о том, что в худшем случае придется перебрать все числа.

👍 Надеюсь стало немного понятнее и теперь, когда в разных книгах или статьях вы встретите записи типа O(n), O(n!), O(n log n) вы не будете впадать в ступор, а будете осознанно понимать, что автор хочет до вас донести.

#junior #algorithm #source
Validation (part 2). Всё еще внутри доменного слоя приложения

Продолжаем рассматривать тему из предыдущего поста. Мы остановились на том, что валидируем сущность перед её созданием, то есть добиваемся того, чтобы в нашей системе все объекты были валидны. Но какие правила туда вообще нужно помещать? От чего объект должен защищать свое состояние?

❗️Объект должен гарантировать что его данные должны быть полными, действительными и консистентными.

📌 Данные неполные (Incomplete) , если для выполнения простых задач отсутствует их логический кусок. Например для Money, если бы у нас была сумма, но отсутствовала валюта. В таком случае мы бы не смогли корректно производить сложение. Более подробно про эту проблему я писал в этом посте.

📌 Недействительные данные (Invalid) — которые имеют правильный тип, но обладают не всеми нужными качествами. Будет проще на примере. Возьмем тот же Money, на данный момент в конструктор мы можем передать любую строку, которая будет обозначать валюту, а это значит, что пользователь может создать объект с несуществующей валютой или той, которая никак не относится к нашему бизнесу (например вы не работает с криптой, а вам передали биткоин). Для выхода из сложившейся ситуации можно (надеемся в php 8.1 уже будет из коробки) использовать Enum (пример для более ранних версий), чтобы убедиться, в корректности переданного значения.

📌 Неконсистентные (Inconsistent) — когда два и более куска данных противоречат друг другу. Например заказ нельзя перевести в статус "доставляется", если нет адреса доставки. То есть помимо текущего состояния объект также несет ответственность за переход между состояниями. Например если заказ оплачен и доставлен — его нельзя просто так "отменить" (подробнее в этом посте).

👉 Связь с другой сущностью по ID

Если в качестве связи с другой сущностью в метод или в конструктор мы передаём ID, то мы наверняка не можем быть уверенны, что Entity с таким ID существует в рамках нашей системы, ведь на входе мы можем убедиться лишь в том, что ID соответствует определенному шаблону (например UUID).

👍 Правильным решением будет — достать сущность из её репозитория, в случае если её не существует мы об этом узнаем. Да и чаще всего нам нужно знать куда больше, чем просто факт существования сущности. Нам будут нужны какие-то её свойства, потому логично будет передавать её в другой объект.

❗️Но не всё так просто. Всё зависит от данных, которые может предоставить Read Model, но это уже совсем другая история, которую позже мы обязательно разберем.

👍 А пока главный месседж — отношения лучше выстраивать с помощью идентификаторов, а не по ссылкам на объект. Во первых таким образом мы понижаем связанность (Low Coupling), а также убираем возможность нежелательных изменений, которые могут происходить внутри связанной сущности.

Вроде по доменной всё. На очереди пользовательская валидация.

#middle #php #oop #source
Repositories (Репозитории)

Каждая сущность (Entity) должна иметь репозиторий. Именно он выступает неким "адаптером" между внешним хранилищем (например БД) и нашим доменным слоем. Но вот проблема.

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

👉 Для решения этой проблемы достаточно создать абстракцию (интерфейс), который и будет принадлежать доменному слою, на который и может быть завязан сервис или юзкейс.

Почему интерфейс репозитория принадлежит доменному слою, а сам репозиторий нет?

Вашему бизнесовому коду всё равно, будете вы использовать базу данных, файловую систему или InMemory для хранения и получения данных, ему важно чтобы были методы с определенными именами, принимали конкретные параметры и возвращали нужные типы данных. Именно это и позволяет оставлять наш код независимым от конкретной реализации, таким образом реализация остаётся в инфраструктурном слое.

❗️ Держите код Repository в чистоте

Очень частое упрощение — весь код, который работает с базой выносим в репозиторий. В итоге получаем, что в репозитории у нас 2 метода, которые мы используем постоянно (например getById() и save()) и 5 методов, которые у нас узконаправлены (например используются для каких-то частных выборок типа отчетов). Здесь вообще стоит немного углубиться в тему Read Model, но пока обойдемся более простой идеей.

👍 Если вам нужен объект из репозитория с ограниченным или наоборот расширенным набором данных только для чтения (то есть вы не предполагаете внесение изменений), то не надо делать этот метод в том-же репозитории. Скорее всего вам нужен Finder, который сразу может вернуть вам удобные структуры данных (тех же DTO). Это удобно как минимум потому, что:

📌 Ваши view не всегда нужны все данные ваших Entity / Агрегатов.
📌 Вам не придётся делать ненужные getters в вашей основной Entity.
📌 Ваши Entity не должны содержать весь список данных "на всякий случай", со всеми ссылками на другие сущности (например Customer со всеми заказами, со всеми данными этих заказов).
📌 В конце концов вы можете разделить источники данных, в то время как Entity могут работать с реляционной, то View могут быть получены из того же Redis, для ускорения получения данных (вопрос консистентности тоже рассмотрим отдельно).

#php #oop #repository #middle #source
🥳🥳🥳 Чатик Beer::PHP 🍺

Я получил уже несколько десятков запросов о создании чата для комментирования статей и обсуждения вопросов. Также иногда я просто не успеваю отвечать на вопросы в личке, так что чатик может стать отличным местом, куда я буду заглядывать с целью ответить, если другой участник не сделает это раньше меня ;)

😢 Грустно это признавать, но наше СНГ-прогерское комьюнити достаточно токсично. В связи с этим в чатах (правда не во всех) много грубых ответов, троллинга, спама и оффтопа, а вот чётких коротких или развернутых ответов на вопросы — мало и их сложно найти за кучей сообщений.

👉 К сожалению в связи с дефицитом времени практически нет возможности его модерировать. Однако, я нашел выход :)

👍👍👍 Вступить в чат можно за символическую (часто просто неподъемную для программиста) подписку 1$ в месяц. Оплата внедрена с целью нажиться на подписч... кхм.. максимально отфильтровать людей и оставить только действительно заинтересованных.

🔨 Если что-то не получается — вот подробная инструкция.

📃 Также выкатил короткий свод правил чатика.

Вступай в чатик, общайся с коллегами и делись своим опытом, мы тебя ждём! ❤️
Validation (part 3). Валидация пользовательских данных

В предыдущих частях (раз, два) мы уже добились того, что наша доменная модель полностью защищена и в случае попадания невалдиных данных наше приложение будет кидать Exception в любой непонятной ситуации.

❗️ Это круто работает если нам надо прервать какую-то операцию, но не слишком удобно, если мы хотим показать пользователю ошибки, особенно если их несколько (например при заполнении формы).

Первое, что приходит в голову, это добавить валидацию в контроллер. При этом у нас уже есть готовые Value Object's, которые используются и доменном слое! Что нам мешает провалидировать Email? Мы точно будем знать, что изменив правила валидации в одном месте, они изменятся везде.

С одной стороны мы убрали дублирование, с другой нет. Мы начинаем переносить всю бизнес логику в контроллер и подсознание говорит, что мы делаем что-то не так. И не надо так делать!

📌 Нам надо просто убедиться, что пользователь не ошибся при вводе данных

👉 Мы должны сделать всё от нас зависящее, чтобы помочь пользователю это сделать. Пока пользователь вводит свои данные — вы уже можете выдать предупреждение, что email выглядит некорректно.

👍 Такую валидацию вы можете доверить фронтовой части вашего приложения (да-да, вообще убрать из контроллера❗️), если работаете c Vue, React, etc..., или доверить встроенным валидаторам фреймворка, если используете twig, blade, etc... Главное помните, что ваша задача помочь пользователю, а не ввести его в ступор своими правилами.

📌 Но есть более сложные бизнес требования

👉 Например во время заказа, вам нужно проверить, что на складе всё еще есть хотя-бы одна единица товара, которую заказывает пользователь (ведь пока он чехлился, кто-то другой мог забрать последнюю единицу первее чем он нажал кнопку). Такие проверки должны выполняться в application слое приложения и отлавливаться через Exception'ы.

📌 Исключения обязательно нужно контролировать

Некоторые, должны быть видны только разработчикам, потому как раскрытие этих данных пользователю сразу создаёт угрозу безопасности. Другие, такие как исключения бизнес логики, должны быть информативными для ваших пользователей.

📌 Вам обязательно нужно различать их в своём коде.

💁‍♂️ Первые могут возникать, когда кто-то пытается абузить ваше приложение, например отправляет рандомные данные POST в обход формы. Такие ошибки должны быть отловлены в инфрастуктурном слое, информация обязательно отправлена в логи, а на фронт выброшен 400 (или более подходящий) ответ без какой-то конкретики.

Чтобы не дублировать подобный код в каждом контроллере — настройте перехватчик в production среде на уровне вашего фреймворка (для development окружения их "глушить" не нужно).

👍 Для вторых круто будет создать пользовательские классы исключений, которые можно отлавливать вместе с другими ошибками формы и отправлять на вывод пользователю. Логированием таких ошибок можно пренебречь, но опять же, всё зависит от вашей задачи.

👉 Использование исключений для пользовательской валидации не должно быть в приоритете. Помощь пользователю при работе с пользовательским интерфейсом — лучший способ пользовательской валидации 😉.

#php #oop #validation #middle #source
Autoload (автозагрузчик)

На всякий случай начнём с основ, т.к. не все знают, как это работает.

👉 Все мы ежедневно создаём классы, которые помещаем в отдельные файлы. Внутри класса мы можем использовать другие классы. И для того, чтобы наш интерпретатор знал о используемом классе, мы должны подключить (require) файл, в котором описан используемый класс. Пока вроде просто. Когда число классов увеличивается, писать все эти require_once становится неудобно. Но выход есть!

❗️ PHP позволяет тебе зарегистрировать автозагрузчик

Сразу пример, как это сделать с помощью функции spl_autoload_register(). Можно зарегистрировать любое число автозагрузчиков (то есть много раз объявить эту самую функцию). Например, если ты подключаешь стороннюю библиотеку, то она может зарегистрировать свой автозагрузчик для загрузки своих классов. Таким образом, каждая библиотека может самостоятельно решать, как она будет искать и подключать файлы.

📁 Если ни один автозагрузчик не подключит файл с вызываемым классом, то будет выведена ошибка об обращении к несуществующему классу. Вот примеры, когда автозагрузчик будет вызван, а когда нет.

👍 Несколько правил, если нужно написать автозагрузчик самостоятельно:

📌 Aвтозагрузчик не должен выдавать ошибку, если он не может найти файл с классом — может быть, этот класс подгрузит следующий автозагрузчик.
📌 Нужно писать автозагрузчик только для своих файлов и не использовать файлы сторонних библиотек.
📌 Не нужно изобретать свои правила сопоставления имен классов и файлов, лучше всего использовать общепринятый стандарт PSR-4 (о нем поговорим позже).

#php #autoload #junior #source
Опросник

👉 Ребзя, хочу улучшить контент, который я выкладываю в этом канале, в связи с чем, сделал этот опросник.

Внутри всего 5 вопросов, которые могут помочь мне лучше понять, что за подписчики тут тусуются, а следовательно лучше адаптировать материал под вас :)

👍 Четыре вопроса обязательных, но там достаточно быстро тыкнуть мышкой. Последний — для творческого полёта мыслей и на него необязательно отвечать, но я буду очень благодарен тем людям, которые там напишут хотя-бы 1-2 предложения.

Короче, залетай, заполняй, помоги автору делать топовый контент!
PSR-4 и Composer autoload

А пока полным ходом идёт заполнение опросника, спешу продолжить тему автозагрузчика. Кстати, реально спасибо всем, кто заполнил опросник. Я не ожидал, что будет столько отклика и полезных ответов. Позже обязательно сведу все результаты и поделюсь с вами, а пока перейдем к материалу =)

😢 Вообще тема PSR достаточно болезненна. Не многие знают, что это не только "правила кодстайла", но и вполне универсальные, стандартизированные концепции, для переиспользования в абсолютно разных проектах.

👉 Начало было положено в 2010 с PSR-0 (Autoloading Standard), который ни много ни мало стал первым шагом к объединению фреймворков, а также безболезненной возможности установки пакетов в ваше приложение (напр. composer в 2012). Однако уже в 2014 ему на смену пришел PSR-4, а PSR-0 был объявлен deprecated.

📌 Согласно PSR-4 мы должны называть файл, взяв название класса и добавив к нему расширение .php, при этом регистр названия класса и файла должны полностью совпадать.

📌 Наш файл должен лежать в каталоге, путь которого совпадает с частями нашего namespace. Например MyModule\Sub\SomeClass будет лежать по пути MyModule/Sub/SomeClass.php.

Обычно в качестве неймспейса верхнего уровня выбирают название приложения / пакета, иногда вместе с названием компании или ником разработчика (например в фреймворке Symfony все классы лежат внутри неймспейса Symfony). По мере роста пакета добавляются дополнительные уровни вложенности и получится что-то вроде \Symfony\Component\HttpFoundation\Request.

Composer autoload

👍 Но, чтобы не писать свой автозагрузчик руками - можно использовать composer.json. Для этого в нём следует создать директиву autoload, во внутри которого прописать "psr-4" и правила, по которым следует сопоставить корневой неймспейс с корневой папкой проекта. Это будет выглядеть так.

❗️ Нужно обратить внимание, что:

1. Важно писать psr-4 именно в нижнем регистре.
2. В качестве разделителя использовать двойной бекслеш \\ — это особенность json, первый он воспринимает как экранирование.
3. В конце неймспейса также следует указывать \\

📌 Есть возможность поиска определенного неймспейса сразу в нескольких директориях, для этого их нужно указать как массив.
📌 Также есть возможность указать fallback каталог, в котором будет искаться любое пространство имён, для этого оставляем неймспейс пустым.

🔗 Все ссылки будут объеденены во время install / update / dump-autoload в один массив, который композер положит в сгенерированный файл vendor/composer/autoload_psr4.php и пробросит путь к нему в основной файл автозагрузки vendor/autoload.php (там могут быть подключены и другие файлы типа psr-0, classmap и т.д.)

🤝 Вам остается только подключить файл в свой проект с помощью require_once __DIR__ . '/vendor/autoload.php'; и можно избавить себя от необходимости писать собственный автозагрузчик.

Также напоминаю, что обсудить любую тему и прокомментировать пост можно в нашем чатике, так что присоединяйся 🍺

#php #psr #autoload #composer #junior #source
Идемпотентные операции

Идемпотентность помогает проектировать более надёжные системы. На самом деле это математическая концепция, которая гласит: идемпотентная операция — это операция, которая не имеет дополнительного эффекта, если она вызывается более одного раза с одними и теми же входными параметрами. Другими словами, если вы выполните одну и ту же операцию несколько раз подряд, то результат не изменится.

Например умножение на 0 и на 1 — идемпотентная операция:

x * 1 == x * 1 * 1
x * 0 == x * 0 * 0

Присваивание — тоже идемпотентная, хотя первый вызов присваивания, можно сказать, имеет побочный эффект, но повторное выполнение не приведет ни к чему другому:

x := 4

Но при чем тут Веб-приложения?

📌 Migrations

👉 Представьте ситуацию: Ваше приложение растёт и перед вами поставили задачу разделить сущность/таблицу Users на данные для доступа Access (напр. login, password, token) и профиля Profile (name, surname, address и т.д.).

Вам нужно написать миграцию, которая создаст 2 новые таблицы в них скопирует необходимые данные пользователей, после чего исходную таблицу переименует в deprecated. Но что если посреди копирования миграция крашнется? Что произойдёт если запустить миграцию еще раз?

👍 Чтобы не беспокоиться об этом — позаботьтесь об идемпотентности ваших миграций. Если ваша БД поддерживает транзакции этих операций — не забудьте их использовать. Если у ваc MySQL, где для операций DDL (CREATE, ALTER, DROP) нет возможности сделать транзакцию, позаботьтесь о том, чтобы использовать CREATE TABLE IF NOT EXISTS, а данные для копирования не просто выбирались полностью (SELECT * FROM USERS), а брались только те, у которых еще нет ряда в Profiles.

📌 Message Queues

👉 Другой пример: Вам надо отправить письма определенной группе пользователей, но посреди отправки вы понимаете, что что-то пошло не так, у вас проблемы с SMTP и ваши письма перестали отправляться. После того как работа SMTP была восстановлена — вам нужно продолжить отправку. Но что будет, если вы запустите команду второй раз?

👍 Для решения — используйте очереди, которые гарантируют доставку. Если по какой-то причине нет такой возможности, то помечайте в базе кому уже письма были отправлены, чтобы они не принимали участия в следующей выборке.

С одной стороны вроде описанные вещи достаточно очевидны, с другой нельзя забывать о них во время проектирования. Всегда старайтесь думать о том, что будет если вам нужно будет повторно выполнить одну и ту же операцию, какие могут быть риски и как избежать головной боли =)

Позже рассмотрим идемпотентность HTTP-методов

#junior #php #architecture #source
Symfony .env и как его готовить

Чем больше я сталкиваюсь с проектами, тем больше вижу, что мало кто использует .env в Symfony так, как это рекомендуют в документации фреймворка. Решил остановиться на этом подробнее.

Как было всегда?

В Symfony до 2018 года и во многих других фреймворках мы использовали комбинацию .env и .env.dist (или .env.example). При этом сам .env не коммится в репозиторий (что логично), а .env.dist имел, как правило, только набор переменных и пустых значений.

С какими проблемами можно столкнуться?

📌 При развертке проекта. Не смотря на то, что названия переменных есть, но сами значения надо где-то взять. Приходится что-то придумывать, у кого-то спрашивать, лезть в разные конфиги, что не очень удобно. Конечно многие выкручиваются тем, что добавляют их прямо в dist, а потом перезаписывают руками.

📌 При добавлении новой переменной всем разработчикам следует сразу добавить её в свой .env, иначе при использовании в контейнере мы быстро отловим Exception. Конечно это тоже можно обойти и задать дефолтные значения прямо в yml.

👉 В целом со всем этим можно жить и нет критических проблем, но что предлагают ребята из Symfony?

❗️ Первое, что бросается в глаза, это коммитить(!) .env. Но спокойно, это сделано, чтобы в данный файл можно было внести настройки по-умолчанию для локальной разработки.

А если даже для локалки не все данные можно коммитить?

👉 Для этого необходимо создать файл .env.local, который будет иметь более высокий приоритет, чем .env, а значит переопределит заданные переменные. Там можно перезаписать необходимые переменные. Все файлы ".local" не коммитятся, то есть для каждого отдельного сервака/компа можно создать и использовать свой .env.local.

👉 Для каждой среды вы можете сделать свои файлы, например .env.dev, .env.test, .env.prod, они также переопределять дефолтные значения, но только в рамках заданного окружения. Эти файлы также коммитятся. Таким образом можно выстроить гибкую иерархию дефолтных значений на все случаи жизни.

👍 Фишки:

📌 При установке некоторых пакетов заботливый Symfony Flex будет сразу добавлять в ваш .env файл необходимые переменные, чтобы вы не забыли об этом. Например после установки Sentry composer require sentry/sentry-symfony, в env появятся вот такие строки.

📌 Реальные переменные окружения (export) имеют преимущество над переменными из файлов. Если нужно — вы можете спокойно экспортировать переменные или сделать source .env и отказаться от использования файла.

📌 Чтобы ускорить продакшн можно использовать composer dump-env prod, который создаст .env.local.php. Это позволит не тратить лишние ресурсы на парсинг .env файла при каждом запросе.

📌 Вы можете переиспользовать уже объявленные переменные внутри своего .env, например:

DB_USER=root
DB_PASS=${DB_USER}-password # присвоит значение root-password

#php #symfony #env #middle #source
🔥1
Введение в Opcode и Opcache

Во время нескольких последних собеседований, которые я проводил, задавал кандидатам на Middle позицию вопрос: Можешь в общих чертах рассказать что такое Opcache, зачем он нужен и как он работает? К сожалению ни один из них даже не попытался. Я был крайне удивлён, ведь в последнее время эта тема была на слуху, благодаря preload и JIT. Но практика показывает обратное. Подумал, что стоит посвятить короткий пост (не вдаваясь в подробности выделяемой памяти и работы ZMM) этой теме.

👉 Все мы знаем, что PHP — интерпретируемый язык. Но что на самом деле происходит с нашим PHP скриптом?

📌 Изначально наш код читается, происходит его разбор и преобразование в так называемые "токены". Это позволяет нашему интерпретатору разбить код на фрагменты, понять где они находятся. Этот процесс называется токенизация или лексирование.

📌 Дальше, имея токены, происходит синтаксический анализ (он также называется parsing), который генерирует абстрактное синтаксическое дерево (Abstract Syntax Tree — AST) для того чтобы было проще понять какие есть операции и какой у них приоритет. На этом этапе приходит анализ тех самых токенов. Уверен, что каждый из вас хоть раз видел ошибку типа:

Parse error: syntax error, unexpected token "==", expecting "(" in script.php on line 10.

и задавался вопросом "что за token"? Теперь тайна раскрыта :)

📌 Дальше происходит компиляция (преобразование) AST в операционный код (Opcode), который наконец и сможет быть выполнен. Не стоит путать, преобразование происходит не в команды ассемблера (очень низкоуровневые), это именно опкоды для виртуальной машины PHP, в них гораздо больше логики.

📌 Далее, виртуальный движок Zend VM (Virtual Machine) получает список наших Opcode и выполняет их! Вот схема всего процесса.

❗️Но после выполнения опкоды немедленно уничтожаются. Возникает вопрос: зачем нам каждый раз токенизирвоать, парсить и компилировать PHP код?

Очень маловероятно, что на production-серверах PHP-код изменится между выполнением нескольких запросов, а значит и Opcode будет точно таким же.

👍 В связи с этим было разработано расширение для кэширования опкодов — Opcache. Его главная задача — единожды скомпилировать каждый PHP-скрипт и закэшировать получившиеся опкоды в общую память, чтобы их мог считать и выполнить каждый рабочий процесс PHP из вашего пула (PHP-FPM). Вот схема с учетом использования Opcache. Расширение Opcache поставляется с PHP.

👌 В результате на запуск скрипта уходит как минимум вдвое меньше времени (сильно зависит от самого скрипта). Чем сложнее приложение, тем выше эффективность этой оптимизации.

#php #middle #opcache #source
🔥2