Saltar al contenido
Curso Claude Code en equipo: una sola forma de trabajar
18/29 Bloque opcional · El paquete Composer de convenciones Arch tests de Pest y código compartido desde un paquete Composer
Tu lectura Quedan 20 min
Lección 18 de 29 · Bloque opcional · El paquete Composer de convenciones

Arch tests de Pest y código compartido desde un paquete Composer

20 min de lectura Claude Code 2.1

Hola, bienvenido. En esta lección vamos a llevar al paquete de convenciones el ViewModel de helpdesk y los 18 arch tests del proyecto. Así, todos los repos del equipo van a comprobar las mismas convenciones con los mismos tests, y una convención que cambia llegará a cada repo junto con el test que la comprueba, en la misma versión del paquete.

En la lección anterior movimos al paquete los documentos que explican las convenciones. Pero lo que de verdad hace cumplir una convención es su arch test, y en el equipo de Marta los arch tests también se copiaron de repo en repo. Un repo tenía 12 y otro 19, porque los que se añadieron después de la copia nunca llegaron al primero. Y hubo un caso peor: cuando el equipo decidió que los servicios tenían que ser readonly, alguien lo cambió en el documento de convención y en algunos repos, pero no en los tests de otros 2, que siguieron aceptando servicios que la convención ya no permitía.

Tu proyecto trae 2 piezas que no dependen de nada propio de helpdesk. Una es el ViewModel de app/ViewModels/ViewModel.php, del que extienden los ViewModels de cada vista. La otra son los 18 arch tests de tests/Architecture/ArchTest.php. Las 2 van a pasar al paquete: el ViewModel, como código que cada repo usa en producción, y los arch tests, como una clase que cada repo llama con una sola línea y que conserva el nombre de cada test.

El ViewModel en el paquete

Vamos a crear las 2 carpetas nuevas del paquete, en la pestaña conventions:

Terminal
mkdir -p src/ViewModels src/Testing

Vamos a crear el ViewModel en conventions/src/ViewModels/ViewModel.php. Es el mismo de tu proyecto, con el namespace del paquete:

conventions/src/ViewModels/ViewModel.php Nuevo +29
1<?php
2
3declare(strict_types=1);
4
5namespace Cursosdesarrolloweb\Conventions\ViewModels;
6
7use Illuminate\Contracts\Support\Arrayable;
8use Illuminate\Support\Str;
9use ReflectionClass;
10use ReflectionMethod;
11
12/**
13 * @implements Arrayable<string, mixed>
14 */
15abstract class ViewModel implements Arrayable
16{
17 /**
18 * @return array<string, mixed>
19 */
20 public function toArray(): array
21 {
22 return collect(new ReflectionClass($this)->getMethods(ReflectionMethod::IS_PUBLIC))
23 ->reject(fn (ReflectionMethod $method): bool => in_array($method->getName(), ['__construct', 'toArray'], true))
24 ->mapWithKeys(fn (ReflectionMethod $method): array => [
25 Str::camel($method->getName()) => $this->{$method->getName()}(),
26 ])
27 ->all();
28 }
29}

El autoload PSR-4 que escribimos en composer.json en la lección anterior ya cubre src/, así que no hay nada que añadir. Es el código que anunciamos en la lección anterior al instalar el paquete en require: cada ViewModel de cada proyecto extiende esta clase, así que tiene que estar también en producción.

El documento de ViewModels del paquete tiene que decir el namespace nuevo, y vamos a aprovechar para cambiar una regla: los ViewModels dejan de recibir el Request y reciben solo los datos que necesitan, que el controller lee de la petición. Así no dependen de HTTP y se prueban pasándoles los datos. Vamos a cambiarlo en conventions/resources/docs/architecture/viewmodels.md:

conventions/resources/docs/architecture/viewmodels.md Modificado +19 −23
4modelo dedicado que centraliza TODA la información que necesita,
5fuera del controller y fuera de la vista.
6
−## La clase base (app/ViewModels/ViewModel.php)
+## La clase base (src/ViewModels/ViewModel.php del paquete)
8
9Los ViewModels NO implementan `Arrayable` uno a uno: **extienden la
−clase base** `App\ViewModels\ViewModel`, que lo hace por ellos con
+clase base** `Cursosdesarrolloweb\Conventions\ViewModels\ViewModel`, que lo hace por ellos con
11reflexión — cada método público se convierte automáticamente en una
12clave camelCase del array:
13
16
17declare(strict_types=1);
18
−namespace App\ViewModels;
+namespace Cursosdesarrolloweb\Conventions\ViewModels;
20
21use Illuminate\Contracts\Support\Arrayable;
22use Illuminate\Support\Str;
64namespace App\ViewModels\Dashboard;
65
66use App\Models\Order;
−use App\ViewModels\ViewModel;
+use App\Models\User;
+use Cursosdesarrolloweb\Conventions\ViewModels\ViewModel;
69use Illuminate\Database\Eloquent\Collection;
−use Illuminate\Http\Request;
70
71final class DashboardViewModel extends ViewModel
72{
73 public function __construct(
− private readonly Request $request,
+ private readonly User $user,
75 ) {}
76
77 public function totalOrders(): int
78 {
− return Order::query()->whereBelongsTo($this->request->user())->count();
+ return Order::query()->whereBelongsTo($this->user)->count();
80 }
81
82 public function recentOrders(): Collection
83 {
84 return Order::query()
− ->whereBelongsTo($this->request->user())
+ ->whereBelongsTo($this->user)
86 ->latest()
87 ->limit(5)
88 ->get();
89 }
90}
91```
92
−Se resuelven por el contenedor: inyección en el método del controller
−(`public function __invoke(DashboardViewModel $viewModel)`) o
−`app(DashboardViewModel::class)`. Pueden recibir el `Request`
−inyectado (filtros, paginación, usuario autenticado).
+Reciben en el constructor los datos que necesitan, **nunca el
+`Request`**: el controller lee de la petición lo que haga falta
+(filtros, usuario autenticado, el modelo de la ruta) y crea el
+ViewModel con `new`. Así el ViewModel no depende de HTTP, su
+constructor dice qué necesita y se prueba pasándole los datos.
98
−Cuando el ViewModel necesita el **modelo de la ruta** (el Upsert con
−`?Order`), se instancia con `new` en el controller — el contenedor no
−puede inyectar el route binding en su constructor. Ambas formas son
−canónicas.
+Un ViewModel sin datos de entrada se resuelve por el contenedor,
+inyectado en el método del controller
+(`public function index(PostListViewModel $viewModel)`).
102
103## Las dos salidas
104
109`$recentOrders`):
110
111```php
−public function __invoke(DashboardViewModel $viewModel): View
+public function __invoke(Request $request): View
113{
− return view('dashboard', $viewModel);
+ return view('dashboard', new DashboardViewModel($request->user()));
115}
116```
117
123```php
124final class PostListViewModel extends ViewModel
125{
− public function __construct(
− private readonly Request $request,
− ) {}
−
126 public function posts(): ResourceCollection
127 {
128 $posts = Post::query()
154## Reglas
155
156- Ubicación: `app/ViewModels/{Dominio}/`. Sufijo `ViewModel`.
−- Clase `final` que **extiende** `App\ViewModels\ViewModel` (hay arch test).
+- Clase `final` que **extiende** `Cursosdesarrolloweb\Conventions\ViewModels\ViewModel` (hay arch test).
158- Un método público = un dato expuesto. Helpers en `private`.
159- Solo lecturas: nunca escribe, nunca llama Actions.
160- Nada de lógica de negocio: solo dar forma a datos para presentación.

Del namespace son 5 líneas: el título del apartado, el namespace en el código de la clase, las 2 menciones de la clase en el texto y el use del ejemplo. El resto es la regla nueva: el apartado que dice cómo recibe sus datos un ViewModel y los 3 ejemplos de código.

Lo que queda de esta lección
  1. 02Los 18 arch tests como una clase del paquete
  2. 03Por qué no un preset de Pest
  3. 04La 1.1.0 del paquete
  4. 05Los arch tests del paquete en helpdesk
  5. 06Los arch tests propios del repo
  6. 07La prueba: 3 incumplimientos a propósito
Sigue leyendo con tu suscripción El curso completo, con su proyecto final, el certificado y el foro, está incluido en la suscripción. Ver los planes