Skip to content

Commit

Permalink
Add transformer example
Browse files Browse the repository at this point in the history
  • Loading branch information
odan committed Jun 3, 2022
1 parent 6dbba28 commit 7566566
Show file tree
Hide file tree
Showing 4 changed files with 38 additions and 93 deletions.
43 changes: 23 additions & 20 deletions src/Action/Customer/CustomerFinderAction.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,48 +2,51 @@

namespace App\Action\Customer;

use App\Domain\Customer\Data\CustomerFinderResult;
use App\Domain\Customer\Service\CustomerFinder;
use App\Renderer\JsonRenderer;
use App\Transformer\CustomerFinderTransformer;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;

/**
* Action.
*/
final class CustomerFinderAction
{
private CustomerFinder $customerFinder;

private JsonRenderer $jsonRenderer;

/**
* The constructor.
*
* @param CustomerFinder $customerFinder The service
* @param JsonRenderer $jsonRenderer The renderer
*/
public function __construct(CustomerFinder $customerFinder, JsonRenderer $jsonRenderer)
{
$this->customerFinder = $customerFinder;
$this->jsonRenderer = $jsonRenderer;
}

/**
* Action.
*
* @param ServerRequestInterface $request The request
* @param ResponseInterface $response The response
*
* @return ResponseInterface The response
*/
public function __invoke(ServerRequestInterface $request, ResponseInterface $response): ResponseInterface
{
// Optional: Pass parameters from the request to the service method
$customers = $this->customerFinder->findCustomers();

$transformer = new CustomerFinderTransformer();
return $this->jsonRenderer->json($response, $this->transform($customers));
}

return $this->jsonRenderer->json($response, $transformer->toArray($customers));
public function transform(CustomerFinderResult $collection): array
{
$customers = [];

foreach ($collection->customers as $customer) {
$customers[] = [
'id' => $customer->id,
'number' => $customer->number,
'name' => $customer->name,
'street' => $customer->street,
'postal_code' => $customer->postalCode,
'city' => $customer->city,
'country' => $customer->country,
'email' => $customer->email,
];
}

return [
'customers' => $customers,
];
}
}
36 changes: 15 additions & 21 deletions src/Action/Customer/CustomerReaderAction.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,42 +2,24 @@

namespace App\Action\Customer;

use App\Domain\Customer\Data\CustomerReaderResult;
use App\Domain\Customer\Service\CustomerReader;
use App\Renderer\JsonRenderer;
use App\Transformer\CustomerReaderTransformer;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;

/**
* Action.
*/
final class CustomerReaderAction
{
private CustomerReader $customerReader;

private JsonRenderer $jsonRenderer;

/**
* The constructor.
*
* @param CustomerReader $companyReader The service
* @param JsonRenderer $jsonRenderer The responder
*/
public function __construct(CustomerReader $companyReader, JsonRenderer $jsonRenderer)
{
$this->customerReader = $companyReader;
$this->jsonRenderer = $jsonRenderer;
}

/**
* Action.
*
* @param ServerRequestInterface $request The request
* @param ResponseInterface $response The response
* @param array $args The routing arguments
*
* @return ResponseInterface The response
*/
public function __invoke(
ServerRequestInterface $request,
ResponseInterface $response,
Expand All @@ -49,8 +31,20 @@ public function __invoke(
// Invoke the domain (service class)
$customer = $this->customerReader->getCustomer($customerId);

$transformer = new CustomerReaderTransformer();
return $this->jsonRenderer->json($response, $this->transform($customer));
}

return $this->jsonRenderer->json($response, $transformer->toArray($customer));
private function transform(CustomerReaderResult $customer): array
{
return [
'id' => $customer->id,
'number' => $customer->number,
'name' => $customer->name,
'street' => $customer->street,
'postal_code' => $customer->postalCode,
'city' => $customer->city,
'country' => $customer->country,
'email' => $customer->email,
];
}
}
30 changes: 0 additions & 30 deletions src/Transformer/CustomerFinderTransformer.php

This file was deleted.

22 changes: 0 additions & 22 deletions src/Transformer/CustomerReaderTransformer.php

This file was deleted.

6 comments on commit 7566566

@samuelgfeller
Copy link

@samuelgfeller samuelgfeller commented on 7566566 Jun 10, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hi Daniel

I'd love to hear a word on what made you decide to not keep the "toArray" method in the corresponding DTO e.g. CustomerFinderResult.php with ArrayReader but instead put a createResult() function in the service class and then a transform() function in the Action?

Is it related to the video you linked in the docs about what DTOs are, where an example is shown accessing 2 different APIs having some different keys in the return structure. If yes, my question is, in your opinion, does it really make sense to separate this like you did when the adapter is not likely to change, or you've made the adapter yourself where you control the keys (like a database)?

On API calls or generally, when the adapter may change, I totally see the usefulness of this, but when you own the adapter in productive projects, it seems to me a bit overly engineered and only more to maintain.

@odan
Copy link
Owner Author

@odan odan commented on 7566566 Jun 10, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think, a DTO should not know "where" the data is coming from, because the data could come from everywhere like different arrays with different keys, any object or something else. Filling a DTO with data should be moved into the (domain) specific context and not within the DTO itself. This makes the DTO much smaller and moves the responsibility to the context specific use case (SRP). A DTO should also not know how the API specific array keys and values should look like, because this is not the responsibility of a DTO (SRP). Instead the domain result should be transformed to an array by using a Transformer.

Transformers are classes, or functions, which are responsible for taking one instance of the resource data and converting it to a basic array.

According to ADR (or MVC 2) the output transformation does belong to the "View" / "Responder" layer. In the past, I implemented this logic within the Domain (service), because it can be very complex, especially pagination.

The new (Transformer) example shows a simplified version of how you "could" separate the domain result from the "view" transformer to render it later to JSON without an extra Transformer class or an extra package like league/fractal. It does not mean that you have to do it like this if you don't feel the need for this extra complexity. Returning the "transformed" (domain) result directly as array, can still be fine.

I know that this extra complexity is not needed in most cases, but I just wanted to give an example for complexer API's. But in bigger projects, it can be quite handy to return a typed domain result (DTO) to simplify the transformation and response-building by separating the domain result from the API result.

At this point, I'm just wondering if I should reduce this Slim skeleton project to an absolute minimum, to be as universal and "unopinated" as possible.

@samuelgfeller
Copy link

@samuelgfeller samuelgfeller commented on 7566566 Jun 10, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think, a DTO should not know "where" the data is coming from, because the data could come from everywhere like different arrays with different keys, any object or something else. Filling a DTO with data should be moved into the (domain) specific context and not within the DTO itself. This makes the DTO much smaller and moves the responsibility to the context specific use case (SRP). A DTO should also not know how the API specific array keys and values should look like, because this is not the responsibility of a DTO (SRP). Instead the domain result should be transformed to an array by using a Transformer.

Okay this makes sense but if we know and specify that a particular data object will only be used to in relation to our database and to retrieve a "result" of a specific module like CustomerReaderResult. Is that not already a context specific use case? Would it be massively wrong in your opinion to actually "give" the responsibility of populating the data object itself to the data object as well as the key names IF we own the source of this specific use case and control the select query?
I kind of like to have set defined keys in the DTO where it populates itself that I know have to exactly match my database column names and on join queries I like the DTO giving strict "rules" as to how to write the select columns. Keeping in mind that the DTO is already use case specific, like a "result".

I don't know if I was clear in explaining what I am thinking.

I know that this extra complexity is not needed in most cases, but I just wanted to give an example for complexer API's. But in bigger projects, it can be quite handy to return a typed domain result (DTO) to simplify the transformation and response-building by separating the domain result from the API result.

Fair enough.

According to ADR (or MVC 2) the output transformation does belong to the "View" / "Responder" layer. In the past, I implemented this logic within the Domain (service), because it can be very complex, especially pagination.

For pagination for instance, would you give the Action / Renderer the responsibility or the Domain?

At this point, I'm just wondering if I should reduce this Slim skeleton project to an absolute minimum, to be as universal and "unopinated" as possible.

I hope my questions don't lead to such conclusions, as it is exactly those "opinionated" things that I don't fully understand at first glance and intrigue me to ask questions and make myself think about the best possible implementation of things.
The question is what do you want to archive with this skeleton project.
What I write next is extremely suggestive and only ties to my experience.
If the main intention is to build a skeleton app for pure professional that exactly know what they're doing, I'd probably have to agree with you, it's best to be as unopinionated as possible.
But if the skeleton is made for people that can program semi-decently but would like to improve, learn new ways and want to write the code as efficient as possible and easiest to maintain, then any opinionated thing can ve very precious. That gives a rare viewpoint for one to see how others (you) do it, compare, think, ask questions and eventually learn.
I for sure learned to program in a way that I find beautiful at first with the Slim-Skeleton where I asked Why are Action classes used over Controller? but its so empty I couldn't learn much. Then and ever since I based my inspiration and also just the reference of "good code" on your skeleton project because it has more features (and opinionated things) than the Slim-Skeleton. There was a real implementation that I could look at on how a user for instance is handled.
Sure there are tutorials and great articles about specific things, but still to this day the slim4-skeleton is the most important project in my learning process as it contains some kind of small scale example of how things can be done on a larger scale, from an experienced developer that I trust.

So if anything, I would love to see more opinionated things. Everybody should know that it is only "a way" to implement things and not "the only valid truth". If someone doesn't like it he/ she is always free to remove it but at least there is something that can be an inspiration for an own implementation. Especially in the beginning, I remember how priceless it was to see chunks of code of an opinionated implementation from you (I count the things related to use case based approach in) as after the apprenticeship I had simply no example of "a good implementation of something" so I didn't even know where to start.
If I recall correctly, I even asked you once if you had an example of a public project where you implement things in the way you did the skeleton project and following a good architecture.
It's because I thought skeleton projects were too minimalistic and not giving enough examples for a lot of things to be really satisfying examples to learn with.
This was the reason that I created the slim-example-project. Genuinely think that there is a void in semi-extensive examples, guidelines of how to cleanly develop a project with PHP without a big framework.

Yesterday the thought to ask you if you'd not want to implement another module that would be linked to Customer crossed my mind, just because I'd love to see how you'd handle a join, data aggregation and everything that comes along it like mixed use case DTO objects like UserPostsData that you suggested to me and much more. I wouldn't have had to ask if I saw a similar implementation in some example.

Anyway, all that to ask you to please NOT make the slim4-skeleton more minimalistic. Sorry for the long read but I wanted to share the important role your skeleton had in my learning and especially those opinionated examples.

@odan
Copy link
Owner Author

@odan odan commented on 7566566 Jun 10, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok, thanks for your nice feedback. I really appreciate it when you ask questions.

Personally, I also think it's important to find good and working examples and get inspired by them. Unfortunately, specific examples are far too rare, and they are even less common in books, because authors wants to reach a wide audience.

Okay this makes sense but if we know and specify that a particular data object will only be used to in relation to our database and to retrieve a "result" of a specific module like CustomerReaderResult. Is that not already a context specific use case?

If I understand you correctly, you are talking about the result of a repository method. In my previous answer, I referred to the domain result and how to separate the result from the domain from the view data by using a DTO. For me, these things are two different things, because the result of a database query is not the same as the result from the domain layer. You can do X queries in the database (using the repository), then combine the result-sets within the service to a completely new result to implement a specific use-case. An endpoint is not an CRUD layer over HTTP. Each endpoint should be designed for each use-case, with its specific input parameters and output (response).

Based on the question regarding the reflection between DTO and table structure, I assume that you probably mean an "entity", which normally represents a record in a table. An Entity is more ORM (Active-Record) related and explicitly something I want to avoid. However, the use-case related approach ensures that the use of entities is not necessary and explicitly not wanted. You can see this by the fact that in this project I just return arrays or primitive data types like int. But if I want to carry data over multiple places, I prefer to use objects (DTO's) instead. Strictly by the book, of course, you should always return objects. I try to me more pragmatic here, for the sake of simplicity. It's always a balance.

@samuelgfeller
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok, thanks for your nice feedback. I really appreciate it when you ask questions.
Personally, I also think it's important to find good and working examples and get inspired by them. Unfortunately, specific examples are far too rare, and they are even less common in books, because authors wants to reach a wide audience.

Glad to hear that, and thank you!

For the next few days/weeks I have to dedicate my energy to business projects to meet deadlines, but as soon as I can work on the example project again, I will read the latter part of your answer carefully and perhaps respond if I still have questions.

@samuelgfeller
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For me, these things are two different things, because the result of a database query is not the same as the result from the domain layer. You can do X queries in the database (using the repository), then combine the result-sets within the service to a completely new result to implement a specific use-case.

I totally agree with that.

I assume that you probably mean an "entity", which normally represents a record in a table. An Entity is more ORM (Active-Record) related and explicitly something I want to avoid.

Well it is kind of related to entity but different in the sense that it doesn't have to reflect a database record row. It could be very use case specific, with keys matching custom names from the join select statements or else. UserPostData is an example.
The most basic form however (the data class, in the style of how you implemented it first) maybe that could count as being seen as entity ? (I don't know, I would love to hear your thoughts on this)

And you say

However, the use-case related approach ensures that the use of entities is not necessary and explicitly not wanted.

Does that count for those "Data" classes too?

I find it pretty comfortable to work with such DTOs and the naming of the keys and array names I just like it consistent from HTML form input names up until the database columns it should always carry the same name. Maybe it would make sense to write it in my naming convention, so it's clear that it's a rule.

Please sign in to comment.