alvarez/concrete-php
最新稳定版本:v1.1.0
Composer 安装命令:
composer require alvarez/concrete-php
包简介
Generator of DTOs and Services for Laravel using stubs
README 文档
README
ConcretePHP — Service Layer & DTO Abstractions
A lightweight, pragmatic set of abstractions to organize business logic in Laravel:
a minimal Service Layer (AbstractModelService) and a compact
DTO system (AbstractDTO).
Table of Contents
-
Overview
-
Features
-
Requirements
-
Installation
-
Quick Start (Examples)
-
Deep Dive — API Reference
AbstractDTOAbstractModelServiceIsDTOcontract
-
Behavior & Implementation Notes (the "magic")
-
Extending & Customization
-
Testing (outside Laravel)
-
Best Practices
-
Packagist / Composer Publishing Tips
-
Contributing
-
Changelog
-
License
1. Overview
ConcretePHP focuses on two complementary building blocks:
- DTOs (Data Transfer Objects) —
AbstractDTOgives you a small, serializable object model for carrying validated data between layers. - Service Layer —
AbstractModelServicewraps an Eloquent model instance and centralizes create/find/update operations while accepting DTOs or arrays.
This combination reduces coupling between HTTP layer and persistence, makes unit-testing trivial, and keeps your controllers focused on request/response logic.
2. Features
- Accept DTOs or arrays uniformly when creating/updating records.
- Fluent interface for updating / setting records.
- Built-in helpers for DTO serialization (
toArray,toJson,fromArray,fromJson), filtering (except) and immutability-like cloning (cloneWith). - Sensible convention:
UserService→App\Models\User(pluggable by overridinggetModelPath). - Lightweight — no repository abstractions, no heavy DI required.
3. Requirements
- PHP 8.0 or later (union types and
staticreturn types are used) - Laravel 8+ (for typical usage), or
illuminate/databaseif you want to use Eloquent standalone.
4. Installation
composer require alvarez/concrete-php
If you plan to run tests or use the package outside Laravel, install Eloquent components:
composer require illuminate/database
5. Quick Start (Examples)
DTO Definition
use Alvarez\ConcretePhp\Data\AbstractDTO; final class CreateUserDTO extends AbstractDTO { public function __construct( public string $name, public string $email, public string $password, ) {} }
Service Definition
use Alvarez\ConcretePhp\Services\AbstractModelService; class UserService extends AbstractModelService { // Add domain-specific helpers here (e.g., changePassword, activate, archive) }
Create a User (DTO or array)
$dto = new CreateUserDTO(name: 'Jane', email: 'jane@example.com', password: bcrypt('secret')); $userService = UserService::create($dto); // or $userService = UserService::create([ 'name' => 'Jane', 'email' => 'jane@example.com', 'password' => bcrypt('secret'), ]);
Find, Update, and Access Model
$service = UserService::find(1); // findOrFail under the hood $service->update(['name' => 'Jane Updated']); $user = $service->getRecord(); // Eloquent model
6. Deep Dive — API Reference
Alvarez\ConcretePhp\Contracts\IsDTO
interface IsDTO { public function toArray(): array; public static function fromArray(array $data): static; public function toJson(): string; public static function fromJson(string $json): self; }
This contract ensures DTOs can be converted to/from arrays and JSON. Services rely on toArray() to persist DTO data.
Alvarez\ConcretePhp\Data\AbstractDTO
Public methods provided:
public static function fromArray(array $data): static— Creates a DTO instance from an array usingnew static(...$data).public function toArray(): array— Returns an associative array of the DTO’s public properties usingget_object_vars($this).public function except(array $keys): array— Returns DTO properties, excluding the provided keys.public function cloneWith(array $values): static— Returns a new DTO instance with merged values (original remains unchanged).public static function fromJson(string $json): static— Create DTO from JSON string.public function toJson(): string— Convert DTO to JSON string.
Notes & caveats
fromArrayuses the argument unpacking operator (...$data) to pass array values to the DTO constructor. This means the order of values matters — the array must provide values in the same order as the constructor parameters. If you prefer keyed mapping, build DTO instances manually or implement a small factory.toArray()returns public properties only. If you use protected/private fields in a derived DTO, they won't be part of the serialized form.
Alvarez\ConcretePhp\Services\AbstractModelService
Public methods provided:
public static function create(array|IsDTO $data): static— Create a new model record and return a Service instance that wraps it. Accepts either a DTO (callstoArray()), or a plain array.public static function find(string|int $id): static— UsesfindOrFailon the resolved model path and returns a Service wrapping the model.public function update(array|IsDTO $data): static— Update the internal model with data from an array or DTO, and return$thisfor chaining.public function getRecord(): Model— Get the underlying Eloquent model instance.public function setRecord(Model $record): static— Replace the stored model instance — returns$this.public static function getModelPath(): string— Resolve the Model FQN by convention. Defaults toApp\Models\{ModelName}derived from service class name.
Behavior details
- When
create()is called with a DTO,create($data->toArray())will be executed on the resolved Eloquent model class. For arrays,create($data)is executed directly. find()usesfindOrFail()to surfaceModelNotFoundExceptionwhen the record is absent (this maps to a 404 in HTTP contexts when using Laravel's exception handler).update()delegates to Eloquent'supdate()method on the stored model instance.
7. Behavior & Implementation Notes (the "magic")
These are the small, opinionated choices that make ConcretePHP feel magical and at the same time predictable — and why they matter:
-
Constructor-wrapped Model:
AbstractModelServicestores a single Eloquent model instance. This makes services lightweight stateful wrappers — ideal for per-request domain operations. -
DTO-first flow: Services accept DTOs and arrays interchangeably. Passing DTOs clarifies intent and ensures you work with validated, explicit contracts before touching persistence.
-
Convention-based model resolution:
getModelPath()extracts the service class basename and removes theServicesuffix.UserService→App\Models\User. Override it if your models live elsewhere or use a different naming scheme. -
Fluent API &
staticreturn types: Methods returnstaticso child service classes preserve fluent chaining and typing. -
Immutability-friendly DTOs:
cloneWith()returns a new instance — original DTOs remain unchanged, which helps reasoning about state when composing domain operations. -
Testing-friendly: The service layer is pure PHP and touches Eloquent only through the Model instance — which means you can inject mocked or in-memory models for unit tests.
8. Extending & Customization
Overriding model path
If your models live outside App\Models, or you want to point to a mock in tests, just override getModelPath():
public static function getModelPath(): string { return Task::class; // or 'App\\MyModels\\Task' }
Add domain methods
A service should contain domain-specific operations, not just CRUD helpers:
class UserService extends AbstractModelService { public function activate(): static { $this->getRecord()->update(['active' => true]); return $this; } public function changePassword(string $password): static { $this->getRecord()->update(['password' => bcrypt($password)]); return $this; } }
Validation & Form Requests
Keep validation responsibility in Form Requests (or a validation layer) and pass a DTO into the service. This keeps the service focused on business logic and persistence.
9. Testing (outside Laravel)
The repository includes example tests which demonstrate how to bootstrap Eloquent in-memory via Illuminate\Database\Capsule\Manager (useful for package tests or library CI):
- Boot Eloquent with an in-memory SQLite DB.
- Create test schema using
Capsule::schema()->create(...). - Define a small mock model (with
$fillable) and a concrete Service class that overridesgetModelPath()to return the mock model class.
Example (conceptual):
$capsule = new Capsule; $capsule->addConnection(['driver' => 'sqlite','database' => ':memory:']); $capsule->setAsGlobal(); $capsule->bootEloquent(); Capsule::schema()->create('tasks', function (Blueprint $t) { $t->id(); $t->string('title'); $t->timestamps(); }); class Task extends Model { protected $fillable = ['title']; } class TaskService extends AbstractModelService { public static function getModelPath(): string { return Task::class; } } // Then use TaskService::create([...]) and assertions as in the package tests.
This pattern is included in the test-suite so Continuous Integration can verify package behavior without a full Laravel app.
10. Best Practices
- Prefer DTOs over raw arrays when passing input to services.
- Keep services focused: orchestrate domain logic and talk to models — do not implement HTTP concerns.
- Use Form Requests (or a Validator) to return validated data before creating DTOs.
- Override
getModelPath()in tests for deterministic behavior. - When using
AbstractDTO::fromArray, ensure the input array values follow the constructor parameter order.
11. Packagist / Composer Publishing Tips
Suggested composer.json excerpt for the package root:
{
"name": "alvarez/concrete-php",
"description": "Lightweight Service Layer and DTO helpers for Laravel and Eloquent",
"type": "library",
"license": "MIT",
"require": {
"php": "^8.0",
"illuminate/database": "^9.0|^10.0"
},
"autoload": {
"psr-4": {
"Alvarez\\ConcretePhp\\": "src/"
}
}
}
- Tag releases semantically (v1.0.0, v1.1.0).
- Add a
CHANGELOG.mdand keep it updated.
12. Contributing
Contributions are welcome!
- Open issues for bugs or feature requests.
- Send PRs against
main. - Keep changes small and focused, and include tests for new behavior.
Suggested CI checks:
- PHPUnit tests
- Static analysis (Psalm/Phan)
- PHPStan level 7+
13. Changelog
See CHANGELOG.md for details. Keep the first release note brief:
v1.0.0— Initial release:AbstractDTO,AbstractModelService, contract and basic tests.
14. License
MIT — see LICENSE for full text.
If you want, I can:
- Produce a shorter
README.mdsuitable for Packagist’s limited preview. - Generate a
composer.jsoncomplete file ready for publishing. - Add ready-to-copy badges and GitHub templates (issue template, PR template).
- Create
CHANGELOG.mdorCONTRIBUTING.mdfiles.
Tell me which of the above you want next and I’ll add them directly to the repository content.
统计信息
- 总下载量: 14
- 月度下载量: 0
- 日度下载量: 0
- 收藏数: 1
- 点击次数: 3
- 依赖项目数: 0
- 推荐数: 0
其他信息
- 授权协议: MIT
- 更新时间: 2026-01-05