Skip to content

Commit e3fe32d

Browse files
committed
Added transformer layer for orm formatting
1 parent 99dd603 commit e3fe32d

File tree

4 files changed

+656
-0
lines changed

4 files changed

+656
-0
lines changed

docs/Documentation/transformers.md

Lines changed: 371 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,371 @@
1+
# Transformers
2+
3+
Transformers provide a clean, consistent way to transform entities, arrays, and collections for API responses. They separate presentation logic from business logic, making your API responses maintainable and testable.
4+
5+
## Overview
6+
7+
The transformer layer allows you to:
8+
- Transform entities and arrays consistently
9+
- Handle collections and paginated results
10+
- Support nested associations (using CakePHP `contain()`)
11+
- Transform CakePHP `matchingData` and `joinData` arrays
12+
- Conditionally include fields based on context
13+
- Format dates and other data types consistently
14+
15+
## Basic Usage
16+
17+
### Creating a Transformer
18+
19+
Create a transformer class that extends `AbstractTransformer`:
20+
21+
```php
22+
<?php
23+
namespace App\Transformer;
24+
25+
use CakeDC\Api\Transformer\AbstractTransformer;
26+
27+
class UserTransformer extends AbstractTransformer
28+
{
29+
public function transform($data): array
30+
{
31+
return [
32+
'id' => (int)$this->get($data, 'id'),
33+
'username' => $this->get($data, 'username'),
34+
'email' => $this->get($data, 'email'),
35+
'created' => $this->timestamp($this->get($data, 'created')),
36+
];
37+
}
38+
}
39+
```
40+
41+
### Using in Actions
42+
43+
Use the `transform()` method in your actions:
44+
45+
```php
46+
<?php
47+
namespace App\Service\AppUsers;
48+
49+
use App\Transformer\UserTransformer;
50+
use CakeDC\Api\Service\Action\CrudAction;
51+
52+
class ViewAction extends CrudAction
53+
{
54+
public function execute()
55+
{
56+
$user = $this->_getEntity($this->_id);
57+
58+
return $this->transform($user, UserTransformer::class);
59+
}
60+
}
61+
```
62+
63+
### Transforming Collections
64+
65+
The `transform()` method automatically handles collections:
66+
67+
```php
68+
<?php
69+
namespace App\Service\AppUsers;
70+
71+
use App\Transformer\UserTransformer;
72+
use CakeDC\Api\Service\Action\CrudIndexAction;
73+
74+
class IndexAction extends CrudIndexAction
75+
{
76+
public function execute()
77+
{
78+
$users = $this->_getEntities();
79+
80+
return $this->transform($users, UserTransformer::class);
81+
}
82+
}
83+
```
84+
85+
## Helper Methods
86+
87+
### `get($data, $key, $default = null)`
88+
89+
Unified data access that works with both entities and arrays:
90+
91+
```php
92+
// Works with entities
93+
$id = $this->get($user, 'id');
94+
95+
// Works with arrays
96+
$id = $this->get($arrayData, 'id');
97+
98+
// Works with CakePHP entities
99+
$email = $this->get($entity, 'email', 'no-email@example.com');
100+
```
101+
102+
### `when($condition, $value, $default = null)`
103+
104+
Conditionally include fields:
105+
106+
```php
107+
public function transform($data): array
108+
{
109+
return [
110+
'id' => $this->get($data, 'id'),
111+
'email' => $this->when(
112+
$this->get($data, 'is_email_visible', true),
113+
$this->get($data, 'email')
114+
),
115+
];
116+
}
117+
```
118+
119+
### `timestamp($date)`
120+
121+
Format dates consistently to ISO 8601:
122+
123+
```php
124+
public function transform($data): array
125+
{
126+
return [
127+
'created' => $this->timestamp($this->get($data, 'created')),
128+
'updated' => $this->timestamp($this->get($data, 'updated')),
129+
];
130+
}
131+
```
132+
133+
### `item($entity, $transformerClass)`
134+
135+
Transform nested entities:
136+
137+
```php
138+
public function transform($data): array
139+
{
140+
$courseType = $this->get($data, 'course_type');
141+
142+
return [
143+
'id' => $this->get($data, 'id'),
144+
'course_type' => $courseType
145+
? $this->item($courseType, CourseTypeTransformer::class)
146+
: null,
147+
];
148+
}
149+
```
150+
151+
### `collection($entities, $transformerClass)`
152+
153+
Transform nested collections:
154+
155+
```php
156+
public function transform($data): array
157+
{
158+
$posts = $this->get($data, 'posts');
159+
160+
return [
161+
'id' => $this->get($data, 'id'),
162+
'posts' => $this->collection($posts, PostTransformer::class),
163+
];
164+
}
165+
```
166+
167+
### `matchingData($matchingData, $transformerClass)`
168+
169+
Transform CakePHP `matchingData` arrays:
170+
171+
```php
172+
public function transform($data): array
173+
{
174+
$matchingData = $this->get($data, '_matchingData');
175+
176+
return [
177+
'id' => $this->get($data, 'id'),
178+
'enrollment_data' => isset($matchingData['Enrollments'])
179+
? $this->matchingData($matchingData['Enrollments'], EnrollmentTransformer::class)
180+
: null,
181+
];
182+
}
183+
```
184+
185+
### `joinData($joinData, $transformerClass)`
186+
187+
Transform CakePHP `joinData` arrays:
188+
189+
```php
190+
public function transform($data): array
191+
{
192+
$joinData = $this->get($data, '_joinData');
193+
194+
return [
195+
'id' => $this->get($data, 'id'),
196+
'pivot_data' => $joinData
197+
? $this->joinData($joinData, PivotTransformer::class)
198+
: null,
199+
];
200+
}
201+
```
202+
203+
## Advanced Examples
204+
205+
### Nested Associations
206+
207+
Transform entities with nested associations loaded via `contain()`:
208+
209+
```php
210+
<?php
211+
namespace App\Transformer;
212+
213+
use CakeDC\Api\Transformer\AbstractTransformer;
214+
215+
class CourseTransformer extends AbstractTransformer
216+
{
217+
public function transform($data): array
218+
{
219+
$courseType = $this->get($data, 'course_type');
220+
$location = $this->get($data, 'location');
221+
222+
return [
223+
'id' => (int)$this->get($data, 'id'),
224+
'course_number' => $this->get($data, 'course_number'),
225+
'start_date' => $this->timestamp($this->get($data, 'start_date')),
226+
'end_date' => $this->timestamp($this->get($data, 'end_date')),
227+
'course_type' => $courseType
228+
? $this->item($courseType, CourseTypeTransformer::class)
229+
: null,
230+
'location' => $location
231+
? $this->item($location, LocationTransformer::class)
232+
: null,
233+
];
234+
}
235+
}
236+
```
237+
238+
### Array Data (JOIN Results)
239+
240+
Transform raw arrays from complex JOIN queries:
241+
242+
```php
243+
<?php
244+
namespace App\Transformer;
245+
246+
use CakeDC\Api\Transformer\AbstractTransformer;
247+
248+
class UserStatsTransformer extends AbstractTransformer
249+
{
250+
public function transform($data): array
251+
{
252+
return [
253+
'user_id' => (int)$this->get($data, 'user_id'),
254+
'username' => $this->get($data, 'username'),
255+
'post_count' => (int)$this->get($data, 'post_count', 0),
256+
'comment_count' => (int)$this->get($data, 'comment_count', 0),
257+
'last_active' => $this->timestamp($this->get($data, 'last_active')),
258+
];
259+
}
260+
}
261+
```
262+
263+
### Conditional Fields with Context
264+
265+
Use context for conditional field inclusion:
266+
267+
```php
268+
<?php
269+
namespace App\Transformer;
270+
271+
use CakeDC\Api\Transformer\AbstractTransformer;
272+
273+
class SecureUserTransformer extends AbstractTransformer
274+
{
275+
protected $context = [];
276+
277+
public function setContext(array $context): void
278+
{
279+
$this->context = $context;
280+
}
281+
282+
public function transform($data): array
283+
{
284+
$isAdmin = $this->context['is_admin'] ?? false;
285+
$authUserId = $this->context['auth_user_id'] ?? null;
286+
$isOwner = $authUserId === $this->get($data, 'id');
287+
288+
return [
289+
'id' => (int)$this->get($data, 'id'),
290+
'username' => $this->get($data, 'username'),
291+
'email' => $this->when($isOwner, $this->get($data, 'email')),
292+
'private_data' => $this->when($isAdmin, $this->get($data, 'private_data')),
293+
];
294+
}
295+
}
296+
```
297+
298+
Usage with context:
299+
300+
```php
301+
<?php
302+
namespace App\Service\AppUsers;
303+
304+
use App\Transformer\SecureUserTransformer;
305+
use CakeDC\Api\Service\Action\CrudAction;
306+
307+
class ViewAction extends CrudAction
308+
{
309+
public function execute()
310+
{
311+
$user = $this->_getEntity($this->_id);
312+
$identity = $this->getIdentity();
313+
314+
$transformer = new SecureUserTransformer();
315+
$transformer->setContext([
316+
'is_admin' => $identity->is_admin ?? false,
317+
'auth_user_id' => $identity->id ?? null,
318+
]);
319+
320+
return $transformer->transform($user);
321+
}
322+
}
323+
```
324+
325+
## Paginated Results
326+
327+
The `transform()` method automatically preserves pagination metadata:
328+
329+
```php
330+
<?php
331+
namespace App\Service\AppUsers;
332+
333+
use App\Transformer\UserTransformer;
334+
use CakeDC\Api\Service\Action\CrudIndexAction;
335+
336+
class IndexAction extends CrudIndexAction
337+
{
338+
public function execute()
339+
{
340+
// If pagination extension is enabled, result will be:
341+
// ['data' => [...], 'pagination' => [...]]
342+
$result = $this->_getEntities();
343+
344+
// transform() preserves pagination structure
345+
return $this->transform($result, UserTransformer::class);
346+
}
347+
}
348+
```
349+
350+
## Best Practices
351+
352+
1. **Keep transformers focused**: Each transformer should handle one entity type
353+
2. **Use helper methods**: Leverage `get()`, `when()`, `timestamp()` for consistency
354+
3. **Handle null values**: Always check for null before transforming nested entities
355+
4. **Use context for conditional fields**: Pass context when you need role-based or user-based field inclusion
356+
5. **Transform at the action level**: Use `$this->transform()` in actions, not in services
357+
6. **Test transformers**: Unit test transformers independently from actions
358+
359+
## Interface
360+
361+
All transformers must implement `TransformerInterface`:
362+
363+
```php
364+
interface TransformerInterface
365+
{
366+
public function transform($data): array;
367+
}
368+
```
369+
370+
The `AbstractTransformer` base class provides common helper methods and implements this interface.
371+

0 commit comments

Comments
 (0)