Атрибуты With v1.0.0
Fluent-сеттеры через #[With], #[WithItem], AbstractWith и WithHandler.
Атрибуты #[With] и #[WithItem] генерируют fluent-сеттеры для private (или protected) свойств. Наследуйте AbstractWith, чтобы __call направлял виртуальные методы в WithHandler.
Быстрый старт
use Devcraft\Abstracts\AbstractWith;
use Devcraft\Attributes\With;
use Devcraft\Attributes\WithItem;
final class Query extends AbstractWith
{
#[With]
private ?int $page = null;
#[With]
private ?string $starting_after = null;
#[With, WithItem('string')]
private array $tags = [];
#[WithItem(['int', 'string'], ['string', 'null'])]
private array $labels = [];
}
$query = (new Query())
->withPage(2)
->withStartingAfter('cursor')
->withTags(['a'])
->withTagsItem('b')
->withLabelsItem('status', 'ready');Канонический пример (с геттерами):
use Devcraft\Abstracts\AbstractWith;
use Devcraft\Attributes\With;
use Devcraft\Attributes\WithItem;
final class Query extends AbstractWith
{
#[With]
private ?int $page = null;
#[With, WithItem('string')]
private array $tags = [];
#[WithItem('string', ['string', 'null'])]
private array $labels = [];
public function page(): ?int
{
return $this->page;
}
public function tags(): array
{
return $this->tags;
}
public function labels(): array
{
return $this->labels;
}
}
$query = (new Query())
->withPage(1)
->withTagsItem('proxy')
->withLabelsItem('status', 'ready');
$query->page(); // 1
$query->tags(); // ['proxy']
$query->labels(); // ['status' => 'ready']См. также: AbstractWith, With, WithItem, WithHandler.
AbstractWith
Devcraft\Abstracts\AbstractWith — рекомендуемый базовый класс:
public function __call(string $methodName, array $arguments): mixed
{
if (WithHandler::handles($this, $methodName)) {
return WithHandler::call($this, $methodName, $arguments);
}
throw new BadMethodCallException(
sprintf('Call to undefined method %s::%s()', $this::class, $methodName)
);
}Можно вызывать WithHandler вручную в своём __call, но наследники AbstractWith получают маршрутизацию бесплатно.
Ограничения свойств
Атрибуты допустимы только на свойствах, которые:
- не public (private или protected)
- не static
- не readonly
#[WithItem] дополнительно требует non-nullable свойство типа array.
Неверная конфигурация бросает LogicException при первой сборке метаданных (первый handles() / call() для класса).
Именование методов
Имена свойств переводятся в StudlyCase:
| Свойство | Метод #[With] | Метод #[WithItem] |
|---|---|---|
$page | withPage($value) | — |
$starting_after | withStartingAfter($value) | — |
$tags | withTags($array) | withTagsItem($item) |
$labels | — | withLabelsItem($key, $value) |
Поиск регистронезависимый (WITHPAGE работает). Имена методов должны быть уникальны по цепочке наследования; коллизии (в том числе только по регистру, $name / $NAME) бросают LogicException.
#[With] — заменить значение целиком
#[With]
private ?int $page = null;
$query->withPage(3); // $page = 3
$query->withPage(null); // допустимо, если свойство nullable- Ровно один аргумент.
- Обычная проверка типа PHP (без coercion string→int для typed properties).
- Возвращает
$this.
#[WithItem] — append или map
Append (один дескриптор типа)
#[WithItem('string')]
private array $tags = [];
$query->withTagsItem('proxy'); // $tags[] = 'proxy'Map (два дескриптора)
#[WithItem('string', ['string', 'null'])]
private array $labels = [];
$query->withLabelsItem('status', 'ready');
$query->withLabelsItem('status', null); // заменитьКлючи map могут быть только int и/или string.
Вместе с #[With]
#[With, WithItem('string')]
private array $tags = [];
$query->withTags(['a', 'b']); // заменить весь массив
$query->withTagsItem('c'); // appendДескрипторы типов
Строка или список строк (union):
#[WithItem('string')]
#[WithItem(['int', 'string'])]
#[WithItem(ItemContract::class)]
#[WithItem(['int', 'string'], ['string', 'null'])]Встроенные: string, int, float, bool, true, false, null, array, object, iterable, callable, mixed.
Также: имена классов, интерфейсов и enum, существующие в runtime.
Замечания:
mixedнельзя комбинировать с другими типами в одном union.floatне принимает integers (без numeric coercion).void/neverотклоняются.- Неизвестные имена классов →
LogicExceptionпри сборке метаданных.
Ошибки runtime vs конфигурации
| Ситуация | Исключение |
|---|---|
| Public / static / readonly свойство | LogicException |
Nullable или не-array цель WithItem | LogicException |
| Плохой count дескрипторов / пустой union / неверные типы ключей map | LogicException |
| Повтор атрибута / коллизия виртуального метода | LogicException |
| Неинициализированный array при append/map | LogicException |
| Неверное число аргументов | ArgumentCountError |
| Значение не проходит проверку дескриптора | TypeError |
Неизвестный виртуальный метод через WithHandler::call | BadMethodCallException |
Неверные аргументы item/map проверяются до мутации — неудачный вызов не частично обновляет массив.
Кэш метаданных
WithHandler строит и кэширует метаданные операций на runtime-класс при первом использовании. Кэш хранит closures, привязанные к declaring class, а не к экземплярам.
Наследование
Атрибуты на private/protected свойствах родителей обнаруживаются и работают на дочерних экземплярах. Writers привязаны к declaring class, поэтому private-свойства родителя остаются записываемыми через виртуальные методы.