Getting started v1.0.0
Install DevCraft Dev Tools, choose a base class, and build a fluent builder or response DTO.
This guide walks through installing DevCraft Dev Tools, choosing the right base class, and building a first fluent builder and a first response DTO.
Requirements
- PHP 8.3 or newer
- Composer
A consuming project that can declare a Composer path repository (or a published package version).
Installation
Path repositories must be configured by the consuming root project. Composer does not inherit them transitively.
Packagist
composer require devcraftclub/dev-toolsLocal path repository
{
"repositories": [
{
"type": "path",
"url": "../DevTool",
"options": { "symlink": true }
}
],
"require": {
"devcraftclub/dev-tools": "@dev"
}
}Then:
composer update devcraftclub/dev-toolsAdjust url to the relative or absolute path of this package on disk. Publish this package later, or replace the path repository with a normal VCS/Packagist repository and a version constraint.
Choosing a base class
| Need | Extend | Property style |
|---|---|---|
| Fluent builders / query objects | AbstractWith | Private (or protected), non-static, non-readonly |
| API response / request DTOs | AbstractReflection | Public, typed, hydratable |
Do not combine them through inheritance. Private fluent properties and public mapped properties solve different problems. If a type needs both shapes, prefer composition or two separate classes.
A real-world fluent example is a WebShare-style query builder: chain withPage(), withPageSize(), and filter helpers, then render a query string. That pattern maps cleanly onto AbstractWith plus #[With] / #[WithItem].
Architecture
Fluent API DTO Mapping
─────────── ───────────
#[With] / #[WithItem] public typed properties
│ │
▼ ▼
AbstractWith ──__call──► WithHandler AbstractReflection
│
▼
ReflectionMapper
│
▼
PropertyValidator
│
Filter / Range / Regex / ArrayOfUse case A — Fluent builder
Extend AbstractWith and annotate private properties:
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']How it works:
AbstractWith::__call()forwards unknown methods toWithHandler.#[With]createswithPropertyName($value)and replaces the whole property.#[WithItem]createswithPropertyNameItem(...). One type descriptor means append; two descriptors mean map set.- Methods return
$this, so chaining is the normal style.
See With attributes for property constraints, naming rules, unions, and error types.
Use case B — API response DTO
Extend AbstractReflection and declare public typed properties, optionally with validation attributes:
use Devcraft\Abstracts\AbstractReflection;
use Devcraft\Attributes\ArrayOf;
use Devcraft\Attributes\Range;
use Devcraft\Attributes\Regex;
final class Address extends AbstractReflection
{
public string $city;
}
final class Proxy extends AbstractReflection
{
#[Regex('/^[0-9a-f-]{36}$/i')]
public string $id;
#[Range(min: 1, max: 65535)]
public int $port;
public Address $address;
#[ArrayOf(Address::class)]
public array $locations = [];
}
$proxy = Proxy::fromArray([
'id' => '550e8400-e29b-41d4-a716-446655440000',
'port' => '8080',
'address' => ['city' => 'Berlin'],
'locations' => [
['city' => 'Berlin'],
['city' => 'Paris'],
],
]);
$proxy->port; // int 8080 (string coerced)
$proxy->address->city; // 'Berlin'
$proxy->toArray(); // nested arrays
echo $proxy->toJson(); // pretty-printed JSONHow it works:
fromArray()creates an instance and hydrates public properties throughReflectionMapper.- Scalars may be coerced from strings where safe (
"8080"→8080,"false"→false). - Nested classes that extend
AbstractReflectionare hydrated recursively. #[ArrayOf]maps list elements to a declared type.- Validation attributes run after conversion; failures raise
ValidationException.
See Reflection mapper and Validation for details.
Running tests in this package
From the DevTool repository root:
composer install
composer testcomposer test runs PHPUnit via phpunit.xml.dist, which bootstraps vendor/autoload.php and discovers everything under tests/.
Next steps
- Read With attributes if you are building fluent APIs.
- Read Reflection mapper if you are mapping API payloads.
- Read Validation to attach
Filter,Range,Regex, orArrayOfrules.