Поддержите проект сделав пожертвование.
Inn_100_gramm

Если вы видите это, значит, я еще не придумал, что написать.

Динамическое подключение трейтов в Laravel: возможно ли это?

В PHP и Laravel трейты являются мощным инструментом для повторного использования кода и улучшения его структуры. Трейты позволяют включать методы в различные классы, избегая дублирования кода и улучшая его читаемость. Однако, возникает вопрос: можно ли динамически подключать трейты к уже существующим объектам в Laravel? В этой статье мы рассмотрим возможности и альтернативные подходы для достижения подобной функциональности.

Почему может понадобиться динамическое подключение трейтов?

Динамическое подключение трейтов может быть полезно в следующих случаях:

Гибкость кода: Возможность изменять поведение объектов в зависимости от контекста или условий. Повторное использование кода: Уменьшение дублирования кода и улучшение структуры приложения. Упрощение тестирования: Легкость замены или изменения функционала для тестирования различных сценариев.

Технические ограничения

Трейты в PHP предназначены для композиции кода на этапе компиляции. Это означает, что они подключаются к классам во время компиляции и не могут быть изменены или добавлены к объектам динамически во время выполнения программы. Тем не менее, есть альтернативные подходы для достижения схожей функциональности.

Альтернативные подходы

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

Шаг 1: Создание трейта

trait LoggableTrait
{
    public function log(string $message): void
    {
        // Логика для логирования
        echo "Log: " . $message;
    }
}

Шаг 2: Создание интерфейса

interface LoggableInterface
{
    public function log(string $message): void;
}

Шаг 3: Реализация интерфейса в классе

class Logger implements LoggableInterface
{
    use LoggableTrait;
}

Шаг 4: Использование композиции

class User
{
    protected LoggableInterface $logger;

    public function __construct(
        protected object $logger
    ) {
    }

    public function performAction(string $action): void
    {
        $this->logger->log("Performing action: " . $action);
        // Другая логика
    }
}

// Пример использования
$logger = new Logger();
$user = new User($logger);
$user->performAction('Login');

Магические методы

Магические методы, такие как __call, позволяют перехватывать вызовы методов и перенаправлять их на другой объект. Этот подход может быть полезен для делегирования вызовов.

Пример реализации с магическим методом __call

class User
{
    public function __construct(
        protected object $logger
    ) {
    }

    public function __call(string $method, array $args)
    {
        if (method_exists($this->logger, $method)) {
            return call_user_func_array([$this->logger, $method], $args);
        }
        
        throw new BadMethodCallException("Method {$method} does not exist.");
    }
}

// Пример использования
$logger = new Logger();
$user = new User($logger);
$user->log('Login');

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

  • Гибкость: Возможность динамически изменять поведение объектов, что упрощает адаптацию к различным условиям и контекстам.
  • Повторное использование кода: Уменьшение дублирования кода и улучшение структуры приложения через инъекцию зависимостей.
  • Легкость тестирования: Упрощение замены или изменения функционала для тестирования различных сценариев.

Заключение

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

0
Inn_100_gramm

Если вы видите это, значит, я еще не придумал, что написать.

Кастомные Query Builders в Laravel

В Laravel часто бывает, что модели содержат слишком много бизнес-логики. К счастью, можно создать собственные классы Query Builders, чтобы сделать модели более “тонкими” и чистыми. В этой статье мы рассмотрим, как создавать и использовать кастомные Query Builders на примере модели Book.

Создание собственного Query Builder Начнем с создания класса BookBuilder. Этот класс будет расширять Illuminate\Database\Eloquent\Builder, чтобы унаследовать всю функциональность стандартного билдера Laravel.

<?php

namespace App\Builders;

use App\Models\User;
use Illuminate\Database\Eloquent\Builder;

class BookBuilder extends Builder
{
    public function wherePublished(): self
    {
        return $this->where('publish_at', '<=', now());
    }

    public function whereAuthor(User $user): self
    {
        return $this->where('author_id', $user->id);
    }

    public function wherePriceBetween(float $from, float $to): self
    {
        return $this->whereBetween('price', [$from, $to]);
    }

    public function whereContains(string $searchTerm): self
    {
        return $this->where(function ($query) use ($searchTerm) {
            $query->where('title', 'LIKE', "%$searchTerm%")
                ->orWhere('description', 'LIKE', "%$searchTerm%");
        });
    }

    public function orderByRatings(): self
    {
        return $this->withAvg('ratings as average_rating', 'rating')
            ->orderByDesc('average_rating');
    }

    public function mostPopular(int $count): self
    {
        return $this->orderByRatings()
            ->take($count);
    }

    public function publish(): self
    {
        $this->model->publish_at = now();
        $this->model->save();

        return $this;
    }
}

Интеграция кастомного Query Builder в модель

Теперь нужно сообщить Laravel, чтобы он использовал наш кастомный билдер при создании запросов для модели Book. Для этого мы переопределим метод newEloquentBuilder в модели.

<?php

namespace App\Models;

use App\Builders\BookBuilder;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\HasMany;

class Book extends Model
{
    use HasFactory;

    public function author(): BelongsTo
    {
        return $this->belongsTo(User::class);
    }

    public function ratings(): HasMany
    {
        return $this->hasMany(Rating::class);
    }

    public function newEloquentBuilder($query): BookBuilder
    {
        return new BookBuilder($query);
    }
}

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

Применение кастомного Query Builder в контроллере

Рассмотрим пример контроллера, который использует наш кастомный Query Builder для фильтрации и сортировки книг.

<?php

namespace App\Http\Controllers;

use App\Models\Book;
use App\Models\User;
use Illuminate\Http\Request;
use Illuminate\Http\JsonResponse;

/**
 * Class BookController
 */
class BookController extends Controller
{
    public function index(Request $request): JsonResponse
    {
        $books = Book::query()
            ->wherePublished()
            ->when($request->authorId, fn($query) => $query->whereAuthor(User::find($request->authorId)))
            ->when($request->fromPrice, fn($query) => $query->wherePriceBetween($request->fromPrice, $request->toPrice))
            ->orderByRatings()
            ->get();

        return response()->json($books);
    }

    public function popular(): JsonResponse
    {
        $books = Book::mostPopular(5)->get();

        return response()->json($books);
    }

    public function publish(Book $book): JsonResponse
    {
        $book->publish();

        return response()->json(['status' => 'success']);
    }
}

Расширение функциональности

С помощью кастомных Query Builders вы можете легко добавлять новые методы для манипуляции данными. Например, метод для публикации всех книг:

<?php

namespace App\Builders;

class BookBuilder extends Builder
{
    // Другие методы...

    public function publishAll(): self
    {
        $this->whereNotPublished()
            ->update(['publish_at' => now()]);

        return $this;
    }

    protected function whereNotPublished(): self
    {
        return $this->where('publish_at', '>', now());
    }
}

Заключение

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

Использование кастомных Query Builders также позволяет лучше организовать бизнес-логику и делает ваши модели более “тонкими”. Этот подход может существенно улучшить качество кода и облегчить его поддержку.

Источник: https://martinjoo.dev/build-your-own-laravel-query-builders

2
Inn_100_gramm

Если вы видите это, значит, я еще не придумал, что написать.

Паттерн "Фильтр" в Laravel

Паттерн “Фильтр” (Filter Pattern) — это архитектурный шаблон, который помогает отделить логику фильтрации данных от контроллеров и моделей. В Laravel этот паттерн часто используется для построения динамических запросов к базе данных на основе входящих данных из HTTP-запросов. Это позволяет сделать код более чистым, поддерживаемым и легко расширяемым.

Пример использования

Рассмотрим пример использования паттерна “Фильтр” на модели Product. Мы создадим фильтр для поиска продуктов по различным критериям, таким как название, категория, цена и наличие на складе.

Шаг 1: Создание фильтра

Создадим базовый класс Filter и конкретный фильтр для продуктов.

<?php

declare(strict_types=1);

namespace App\Core\Filters;

use Carbon\CarbonImmutable;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Support\Str;

/**
 * Class Filter
 */
abstract class Filter
{
    public const KEYS_TO_BOOL = [];
    public const KEYS_TO_INT = [];
    public const KEYS_TO_DATE = [];
    public const KEYS_STRING_TO_ARRAY = [];
    public const KEYS_TO_ARRAY = [];

    protected Builder $builder;

    /**
     * @param FormRequest $request
     */
    public function __construct(protected readonly FormRequest $request)
    {
    }

    /**
     * Применение фильтров к запросу
     *
     * @param Builder $builder
     * @return Builder
     */
    public function apply(Builder $builder): Builder
    {
        $this->builder = builder;

        foreach ($this->request->input() as $method => $value) {
            $methodName = Str::camel($method);

            if (null === $value) {
                continue;
            }

            if (method_exists($this, $methodName)) {
                if (in_array($method, static::KEYS_TO_BOOL, true)) {
                    $value = (bool)$value;
                }

                if (in_array($method, static::KEYS_TO_INT, true)) {
                    $value = (int)$value;
                }

                if (in_array($method, static::KEYS_TO_DATE, true)) {
                    $value = CarbonImmutable::parse($value);
                }

                if (in_array($method, static::KEYS_TO_ARRAY, true)) {
                    $value = is_array($value) ? $value : [$value];
                }

                if (in_array($method, static::KEYS_STRING_TO_ARRAY, true)) {
                    $value = explode(',', $value);
                }

                $this->builder = $this->{$methodName}($value);
            }
        }

        return $this->builder;
    }
}

Создадим конкретный фильтр для продуктов.

<?php

namespace App\Filters;

use Illuminate\Database\Eloquent\Builder;

/**
 * Class ProductFilter
 */
class ProductFilter extends Filter
{
    /**
     * Фильтрация по названию
     *
     * @param string $value
     * @return Builder
     */
    protected function name(string $value): Builder
    {
        return $this->builder->where('name', 'like', '%' . $value . '%');
    }

    /**
     * Фильтрация по категории
     *
     * @param string $value
     * @return Builder
     */
    protected function category(string $value): Builder
    {
        return $this->builder->where('category', $value);
    }

    /**
     * Фильтрация по цене
     *
     * @param array $value
     * @return Builder
     */
    protected function price(array $value): Builder
    {
        return $this->builder->whereBetween('price', [$value['min'], $value['max']]);
    }

    /**
     * Фильтрация по наличию на складе
     *
     * @param bool $value
     * @return Builder
     */
    protected function inStock(bool $value): Builder
    {
        return $this->builder->where('in_stock', $value);
    }
}

Шаг 2: Создание запроса

Создадим FormRequest, который будет использоваться для валидации и передачи данных в фильтр.

<?php

namespace App\Http\Requests;

use Illuminate\Foundation\Http\FormRequest;

/**
 * Class ProductFilterRequest
 */
class ProductFilterRequest extends FormRequest
{
    /**
     * Правила валидации
     *
     * @return array
     */
    public function rules(): array
    {
        return [
            'name'     => 'nullable|string|max:255',
            'category' => 'nullable|string|max:255',
            'price'    => 'nullable|array',
            'price.min'=> 'nullable|numeric|min:0',
            'price.max'=> 'nullable|numeric|min:0',
            'in_stock' => 'nullable|boolean',
        ];
    }
}

Шаг 3: Использование трейт для применения фильтра

Создадим трейт HasFilter, который позволит легко применять фильтры к моделям.

<?php

declare(strict_types=1);

namespace App\Core\Traits;

use App\Core\Filters\Filter;
use Illuminate\Database\Eloquent\Builder;

/**
 * Trait HasFilter
 *
 * @method static Builder filter(Filter $filter)
 */
trait HasFilter
{
    /**
     * @param Builder $builder
     * @param Filter $filter
     * @return Builder
     */
    public function scopeFilter(Builder $builder, Filter $filter): Builder
    {
        return $filter->apply($builder);
    }
}

Шаг 4: Применение фильтра в контроллере

Теперь используем наш фильтр в контроллере для получения продуктов.

<?php

namespace App\Http\Controllers;

use App\Filters\ProductFilter;
use App\Http\Requests\ProductFilterRequest;
use App\Models\Product;
use Illuminate\Http\JsonResponse;

/**
 * Class ProductController
 */
class ProductController extends Controller
{
    /**
     * Получение списка продуктов с фильтрацией
     *
     * @param ProductFilterRequest $request
     * @param ProductFilter $filter
     * @return JsonResponse
     */
    public function index(ProductFilterRequest $request, ProductFilter $filter): JsonResponse
    {
        $products = Product::filter($filter)->get();

        return response()->json($products);
    }
}

Заключение

Паттерн “Фильтр” позволяет легко и элегантно управлять сложными запросами к базе данных, отделяя логику фильтрации от контроллеров и моделей. Это улучшает читаемость кода и упрощает его поддержку. Такой подход также облегчает тестирование и расширение логики фильтрации без необходимости изменения основного кода. Использование трейт HasFilter дополнительно упрощает применение фильтров к моделям, делая код еще более модульным и переиспользуемым.

2