Библиотека пхпшника | PHP, Laravel, Symfony, CodeIgniter
10.7K subscribers
1.65K photos
27 videos
26 files
4.44K links
Все самое полезное для пхпшника в одном канале.

По рекламе: @proglib_adv

Учиться у нас: https://proglib.io/w/bca892d6

Для обратной связи: @proglibrary_feeedback_bot

РКН: https://gosuslugi.ru/snet/67a5d13cd6fa92100ee6f68b
Download Telegram
Точки роста PHP-разработчика

Наш подписчик спрашивает

«Я PHP-разработчик с опытом 1,5 года. Но ощущаю, что топчусь на месте. Какие навыки стоит развивать, чтобы сдвинуть с мертвой точки через год-два ? Углубляться в фреймворки или лучше прокачивать soft skills?»

Алексей, PHP Developer


💬 Делитесь в комментариях:

— Какие технические навыки необходимы для роста
— Насколько важны фреймворки и архитектура
— Какие soft skills действительно ценят

P.S. Если хотите задать вопрос, заполните нашу гугл-форму. Это займет 5 минут.

#междусобойчик
Please open Telegram to view this post
VIEW IN TELEGRAM
🔥3😁21
🔍 Убийца перфоманса, который можно пропустить в ревью

Классика, которую все знают, но продолжают допускать. Особенно в Eloquent.

🔹 Проблема

$orders = Order::all();

foreach ($orders as $order) {
echo $order->user->name; // SELECT * FROM users WHERE id = ? (на каждой итерации!)
}


🔹 Решение — eager loading

$orders = Order::with('user')->get();
// Теперь 2 запроса вместо N+1


🔹 Как поймать в проде

Подключите debugbar или telescope в dev-среде. Или логируйте запросы вручную:

DB::listen(function ($query) {
Log::info($query->sql, $query->bindings);
});


💡 Если видите один и тот же запрос десятки раз в логах — это он. Ищите все места, где обращаетесь к связям внутри цикла.

Библиотека пхпшника

#vardump
Please open Telegram to view this post
VIEW IN TELEGRAM
🥱8👍5👏1😢1
📊 readonly properties vs private + getter

До 8.1 иммутабельность в value objects эмулировали через private + геттер. Это работало, но создавало шум: конструктор, приватное свойство, метод. Три места ради одной идеи.

// До
class Money {
private int $amount;

public function __construct(int $amount) {
$this->amount = $amount;
}

public function getAmount(): int {
return $this->amount;
}
}


readonly убирает весь этот церемониал:

// После
class Money {
public function __construct(
public readonly int $amount,
) {}
}


Читается как контракт: свойство публично, но записать его можно только один раз при инициализации. Повторная запись → Error, не предупреждение.

Когда readonly не подходит:

1. Нужна ленивая инициализация. readonly нельзя присвоить вне конструктора (до PHP 8.4 с хуками).
2. Клонирование с изменением. clone с readonly требует PHP 8.3 (readonly классы + clone в new static).
3. Моки в тестах. Если тест хочет подменить значение readonly-свойства через рефлексию, получит Error.

PHP 8.2 добавил readonly классы — все свойства автоматически readonly:

readonly class Coordinates {
public function __construct(
public float $lat,
public float $lng,
) {}
}


Хорошо для DTO, value objects, команд и событий. Плохо там, где нужна мутация или частичная инициализация.

Правило простое: если объект концептуально иммутабелен → readonly. Если есть хоть одна причина менять состояние после создания → private + логика в методах.

🐸 Библиотека пхпшника

#элементарный_выбор
Please open Telegram to view this post
VIEW IN TELEGRAM
👍8🔥2🥱1
💬 Давайте обсудим

К статье на Хабре «Я два месяца платил 300к человеку, который тихо скармливал мои задачи в ChatGPT» уже почти 900 комментов.

Что думаете, пишите в комменты?

#междусобойчик
Please open Telegram to view this post
VIEW IN TELEGRAM
😁10👍1
⌨️ Топ-вакансий по PHP за неделю

PHP Developer (Laravel)
- 150 000 - 200 000₽ - офис/гибрид/удалёнка (Ульяновск)

Backend-разработчик (PHP) - до 220 000 ₽ - офис/гибрид (Москва)


Старший fullstack-разработчик PHP (Laravel) - от 300 000 до 350 000 ₽ - удалёнка

➡️ Еще больше топовых вакансий — в нашем канале PHP Jobs
Please open Telegram to view this post
VIEW IN TELEGRAM
2👍1
🧠 PHP-лайфхак

Хочешь ловить баги до запуска кода? Используй статический анализ.
Это инструмент, который проверяет твой PHP-код на ошибки, несоответствия типам и подозрительные конструкции — без выполнения программы.

🔍 Как это работает

🧩 Установи PHPStan:
composer require --dev phpstan/phpstan

Запусти анализ:
./vendor/bin/phpstan analyse src --level=max

--level=max включает максимально строгую проверку.
Можно использовать уровни от 0 до 9 — начни с 5–6, чтобы не утонуть в предупреждениях.

⚡️ Что даёт PHPStan / Psalm

🐞 Находит ошибки типов ещё до запуска кода

🚫 Предупреждает о неиспользуемых переменных и мёртвом коде

🧩 Помогает навести порядок в архитектуре

🔒 Снижает риск багов при рефакторинге

💡 Интеграция с Composer и CI/CD

Добавь команду в composer.json:
"scripts": {
"analyze": "phpstan analyse src --level=max"
}


Теперь можно запускать проверку просто:

composer analyze

А в CI — добавить шаг перед деплоем, чтобы ни один коммит с ошибками типов не ушёл в прод.

Библиотека пхпшника
👍111🔥1
 ♻️ Redis-кеш с AI

Настроить кеш через Redis несложно. Но зачем копировать код из старого проекта, бояться что-то потерять, когда можно сгенерировать эту часть.

Промпт, который закроет этот вопрос:

Ты Senior PHP-разработчик. Сгенерируй модуль кеширования для Laravel с Redis, с учётом всех современных паттернов, устойчивости и тестирования.
Требования:
Использование Redis через phpredis или predis (с учётом конфигурации в .env)
Кеширование с разными TTL для разных типов данных (разделение по Cache Store)
2 сервиса с кешированием:
Стратегия cache-aside (чтение из кеша и обновление вручную)
Стратегия write-through (автоматическое обновление кеша при записи данных)
Обработка инвалидации кеша при изменении данных
Кастомная обработка ошибок:
Fallback на in-memory или file cache (без исключений при недоступности Redis)
Логирование ошибок кеширования с уведомлением
Применение circuit breaker и retry паттернов при сете ошибок Redis
Защита от слишком частых запросов через кеширование с TTL и expiration для разных кешей
Изоляция кеш-слоя с использованием сервисов и репозиториев (не Cache фасад в контроллерах)
Настройки в .env и конфигурации кеша
Простой и понятный конфиг с разделением кешей для разных типов данных
Юнит-тесты с использованием Testcontainers для запуска Redis в Docker
Тестирование fallback-механизмов
Мокирование с имитацией недоступности Redis
Тесты на использование кеша с разными TTL
Стек: PHP 8.1+, Laravel 9.x, Redis, PHPUnit, Docker, Testcontainers


Библиотека пхпшника
👍7🥱21🔥1
😱 Если ваш продукт не умеет отдавать данные в формате, понятном AI-агенту, то вас просто не существует

Скрипт не будет кликать по красивым кнопкам в браузере, он уйдёт к конкуренту с нормальным API. Перестроить архитектуру под машинных клиентов — это уже не хайп, а необходимое условие сохранения конкурентоспособности.

Как адаптировать продукт и не исчезнуть из выдачи:

— интегрировать MCP и A2A-взаимодействие, чтобы агенты могли вас читать;
— научиться контролировать стоимость (лимиты, кэш, роутинг между моделями);
— настроить AgentOps: трейсинг, логирование и отлов регрессий.

Всё это ждёт вас на обновлённом курсе «Разработка AI-агентов». Мы специально сделали фокус на утилитарном инжиниринге и production-ready решениях.

Кстати, до 29 марта можно забрать курс с большой скидкой, и стоит поторопиться — мест на потоке всё меньше.

Зафиксировать цену и начать деплоить агентов без слива бюджета 👈
1
🧠 array_map vs foreach

Многие считают array_map более "функциональным" и современным. Но есть нюанс.

Бенчмарк на массиве из 100 000 элементов:

// array_map — ~12ms
$result = array_map(fn($x) => $x * 2, $items);

// foreach — ~6ms
$result = [];
foreach ($items as $x) {
$result[] = $x * 2;
}


foreach в 1.5–2 раза быстрее на больших массивах, потому что array_map создаёт замыкание и имеет overhead на каждый вызов callback.

Когда всё равно использовать array_map?

Когда важна читаемость, а массив маленький (до ~1000 элементов) — разница незаметна. Не оптимизируйте там, где это не нужно.

// Читаемо и достаточно быстро для небольших коллекций
$emails = array_map(fn($u) => $u->email, $users);


💡 Правило: сначала профилируйте, потом оптимизируйте. Blackfire и Xdebug в помощь.

Библиотека пхпшника

#vardump
Please open Telegram to view this post
VIEW IN TELEGRAM
👍202🔥1
✔️ PHP-тест: Exception handling + PDO транзакции + молчаливая потеря данных

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

📦 Задание

Есть сервис для обработки платежей. Код покрыт тестами, транзакции есть, ошибки логируются. На проде раз в несколько дней часть платежей пропадает — в БД нет записи, в логах нет ошибок, пользователь уверен что оплатил.

// src/Payment/PaymentService.php
class PaymentService
{
public function __construct(
private PDO $pdo,
private Logger $logger,
private Notifier $notifier,
) {}

public function process(PaymentDTO $dto): bool
{
try {
$this->pdo->beginTransaction();

$paymentId = $this->insertPayment($dto);
$this->updateBalance($dto->userId, $dto->amount);
$this->insertAuditLog($paymentId, $dto);

$this->pdo->commit();

$this->notifier->sendReceipt($dto->userId, $paymentId);

return true;

} catch (NotificationException $e) {
$this->logger->warning('Receipt failed', ['error' => $e->getMessage()]);
return true;

} catch (Throwable $e) {
$this->logger->error('Payment failed', ['error' => $e->getMessage()]);
$this->pdo->rollBack();
return false;
}
}

private function insertPayment(PaymentDTO $dto): int
{
$stmt = $this->pdo->prepare(
'INSERT INTO payments (user_id, amount, status) VALUES (?, ?, ?)'
);
$stmt->execute([$dto->userId, $dto->amount, 'pending']);
return (int) $this->pdo->lastInsertId();
}

private function updateBalance(int $userId, float $amount): void
{
$stmt = $this->pdo->prepare(
'UPDATE balances SET amount = amount - ? WHERE user_id = ?'
);
$stmt->execute([$amount, $userId]);

if ($stmt->rowCount() === 0) {
throw new \RuntimeException("Balance record not found for user $userId");
}
}

private function insertAuditLog(int $paymentId, PaymentDTO $dto): void
{
// Пишем в отдельную audit БД через отдельное соединение
$this->auditPdo->prepare(
'INSERT INTO audit_log (payment_id, user_id, amount) VALUES (?, ?, ?)'
);
// ... execute
}
}


🔹 Задачи

— Найти сценарий, при котором платёж коммитится в БД, но return true не доходит до контроллера — и данные считаются потерянными
— Объяснить проблему
— Предложить исправленную структуру

Ставьте → 🔥 если нравится формат. Если нет → 🌚

💬 Решения пишите в комменты под спойлер — сравним подходы.
Please open Telegram to view this post
VIEW IN TELEGRAM
🔥12👍4🌚32
📊 str_contains / str_starts_with vs strpos

strpos возвращает int|false. Это создаёт одну конкретную проблему: проверку на false нужно делать строго:

// Классическая ошибка
if (strpos($url, 'admin')) {
// Не выполнится, если 'admin' в позиции 0
}

// Правильно
if (strpos($url, 'admin') !== false) {
// Окей
}


Этот паттерн !== false живёт в кодбазах как ритуал, смысл которого половина команды уже не помнит.

PHP 8.0 дал str_contains, str_starts_with, str_ends_with. Они возвращают bool. Просто bool.

if (str_contains($url, 'admin')) { ... }
if (str_starts_with($route, '/api')) { ... }
if (str_ends_with($file, '.blade.php')) { ... }


Никакого !== false. Никакой магии с позицией 0.

Что осталось за strpos:

— Нужна позиция вхождения, а не факт наличия
— Нужен offset для поиска со смещением
— substr_count, substr_replace всё ещё работают с позициями

$pos = strpos($text, '{{');
$end = strpos($text, '}}', $pos);
// Здесь strpos оправдан — тебе нужны числа


Про производительность: str_contains не медленнее strpos на коротких строках. На длинных — профилируй конкретный кейс, не гадай.

Короткий вывод: str_contains и компания — это не синтаксический сахар. Это правильный тип возврата для правильного вопроса.

🐸 Библиотека пхпшника

#элементарный_выбор
Please open Telegram to view this post
VIEW IN TELEGRAM
👍112
📊 Repository Pattern vs Query Builder

Repository — это не про «обернуть Eloquent в класс». Это про то, что доменный код не должен знать, откуда берутся данные.

Плохой Repository, который можно часто встретить:

class UserRepository {
public function findActive(): Collection {
return User::where('status', 'active')->get();
}

public function findByEmailAndStatus(string $email, string $status): ?User {
return User::where('email', $email)
->where('status', $status)
->first();
}
// ...ещё 40 методов под каждый запрос
}


Это не Repository — это коллекция запросов в обёртке. Домен по-прежнему диктует методы через свои нужды, а репозиторий распухает.

Хороший Repository работает с агрегатами и скрывает детали хранения:

interface UserRepository {
public function findById(UserId $id): ?User;
public function findByEmail(Email $email): ?User;
public function save(User $user): void;
public function remove(User $user): void;
}


Всё остальное — спецификации, критерии, query objects. Или честный Query Builder там, где это не домен.

Где Query Builder напрямую оправдан:

— Read-модели (CQRS: команды через домен, запросы — прямо в БД)
— Отчёты, дашборды, агрегации — там нет смысла гидрировать объекты
— Простые CRUD-экраны без доменной логики

// Это нормально для read-модели
$stats = DB::table('orders')
->selectRaw('status, COUNT(*) as count, SUM(amount) as total')
->groupBy('status')
->get();


Пытаться пропустить это через Repository с Order-сущностями — оверинжиниринг.

Правило: Repository живёт на границе домена. Если у тебя нет домена, нет смысла в Repository. Если домен есть, Repository скрывает инфраструктуру, а не просто переносит запросы в другой файл.

🐸 Библиотека пхпшника

#элементарный_выбор
Please open Telegram to view this post
VIEW IN TELEGRAM
6😁5🥱2👍1
⌨️ Топ-вакансий по PHP за неделю

PHP разработчик - от 150 000 до 200 000 ₽ - удалёнка

Senior Fullstack PHP разработчик - от 250 000 до 350 000 ₽ - удалёнка

Lead PHP Developer - до 400 000 ₽ - удалёнка

➡️ Еще больше топовых вакансий — в нашем канале PHP Jobs
Please open Telegram to view this post
VIEW IN TELEGRAM
1👍1😁1
✌🏻 У нас две новости — хорошая и плохая!

Хорошая: Ваших знаний, скорее всего, хватит, чтобы собрать рабочую демку AI-агента в Colab. 🫡

Плохая: Вы вряд ли выведете его в прод, не обанкротившись на токенах и не слив базу. 🤯

Для защиты от таких сценариев мы полностью пересобрали курс «Разработка AI-агентов». Теперь внутри плотная работа с экономикой ресурсов, дебаг через time-travel в LangGraph, извлечение данных из кривых сканов для RAG и комплаенс по 152-ФЗ.

Если всё ещё сомневаетесь, послушайте голосовое от спикера курса Влада Прошинского, где он объясняет, как правильно тестировать агентов перед релизом.


Программа курса, полный состав спикеров и другие подробности 👈🏻

ВАЖНО! До 5 апреля на курс действует скидка, но свободные места могут закончиться раньше.
😢2🥱1
📊 Queue vs Cron

Оба инструмента про «выполнить что-то не прямо сейчас». Но они решают разные задачи, и смешивать их не стоит.

Cron — планировщик. Он про время. «Запусти это в 03:00». «Запусти каждые 5 минут». Ему всё равно, что именно запускать — он просто триггерит процесс по расписанию.

Queue — очередь. Она про события и нагрузку. «Отправь письмо, когда пользователь зарегистрировался». «Обработай 10 000 строк CSV асинхронно». Очередь не знает про время, она знает про задачи.

🔹 Где Cron уместен

— Агрегация данных за период (ночные отчёты)
— Очистка устаревших записей
— Синхронизация с внешним источником по расписанию
— Задачи, которые должны выполняться независимо от пользовательской активности

🔹 Где Queue уместен

— Всё, что пользователь спровоцировал, но ждать не должен
— Задачи с потенциальными ретраями (внешний API может упасть)
— Параллельная обработка (несколько воркеров)
— Отправка уведомлений, генерация файлов, вебхуки

Если ты пишешь cron-задачу, которая ищет записи со статусом "pending" — ты уже строишь очередь. Используй нормальную: Redis + Laravel Horizon, RabbitMQ, SQS, Beanstalkd.

Комбинация: cron запускает job, который кладёт задачи в очередь. Это нормально — диспетчер по расписанию и обработчики асинхронно.

🐸 Библиотека пхпшника

#элементарный_выбор
Please open Telegram to view this post
VIEW IN TELEGRAM
👍9🥱52
✔️ PHP-тест: Generators + Memory leak + Batch-обработка

«Мы перешли на генераторы, чтобы не грузить память» — но память всё равно растёт 👇

📦 Задание

Команда переписала импорт CSV на генераторы — раньше падало с OOM на файлах больше 500 МБ. После рефакторинга память перестала расти... на стейджинге. На проде с реальными файлами на 2M+ строк потребление всё равно ползёт вверх.

// src/Import/CsvImporter.php
class CsvImporter
{
private array $processedIds = [];
private array $errors = [];
private int $totalRows = 0;

public function import(string $filePath): ImportResult
{
foreach ($this->readRows($filePath) as $row) {
$this->totalRows++;

try {
$id = $this->processRow($row);
$this->processedIds[] = $id;
} catch (RowException $e) {
$this->errors[] = [
'row' => $this->totalRows,
'message' => $e->getMessage(),
'data' => $row,
];
}
}

return new ImportResult($this->processedIds, $this->errors, $this->totalRows);
}

private function readRows(string $filePath): \Generator
{
$handle = fopen($filePath, 'r');
$headers = fgetcsv($handle);

while (($raw = fgetcsv($handle)) !== false) {
yield array_combine($headers, $raw);
}

fclose($handle);
}

private function processRow(array $row): int
{
// Валидация, маппинг, вставка в БД
// Возвращает inserted ID
return $this->repository->upsert($row);
}
}

// src/Import/ImportResult.php
class ImportResult
{
public function __construct(
public readonly array $processedIds,
public readonly array $errors,
public readonly int $totalRows,
) {}
}


🔹 Задачи

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

Ставьте → 🔥 если нравится формат. Если нет → 🌚

💬 Решения пишите в комменты под спойлер — сравним подходы.
Please open Telegram to view this post
VIEW IN TELEGRAM
🔥134👍1