PHP/MVC : in this case, how to use DTO? - php

I'm new to OOP and MVC with PHP, and I'm currently learning by making my own custom framework from scratch, for testing purposes. I have set up my controllers, models and views and everything works fine.
My app has the following architecture :
It’s a small blog that follows the rules of the MVC pattern. To summarize, it works like this :
The called Controller will fetch the data using the right models
Models return objects of the class \Classes\{MyObject}
Controller call the right template to render the view, and passes it the data and objects to display
The problem
In some views, I need to display related data. For example, in the article view, I need to display the author's first name. In the database, an article contains only the author’s ID, not his first name : this is the same thing in my class \Classes\Article.
What I've tried
To display the author’s first name in my view, I've updated the model Find method to use a LEFT JOIN in the SQL query. Then, I've updated my \Classes\Article class to have a user_firstname property :
class Article
{
private $pk_id;
private $title;
private $excerpt;
private $content;
private $created_at;
private $fk_user_id;
private $updated_at;
private $user_firstname; // <-- I've added this property to retrieve author's firstname
// (...)
}
What I did works well, but my teacher tells me it’s not the right way to do it because the author’s firstname is not part of the definition of an article.
In this case, my teacher tells me to use a DTO (Data Transfert Object) between Article and User classes.
Questions
What is the right way to set up a DTO in this case?
Do I need to create a new ArticleUserDTO class in a new namespace ?
How to use it ?
I think I understood the problem : the Article class should only contain what defines an article. But I can’t understand the logic of setting up a DTO. I’ve done some research on it, I understand the usefulness of the DTO but I can’t set up into my app.

Setting up a DTO in my app was easy ! As mentioned by #daremachine, the diagram below helped me to understand what a DTO is for.
Diagram source : martinfowler.com
We can see DTOs as an object assembler, in which we place all the elements we need on the view side.
For example, in the post view of an article, I needed to display other items, such as the author and posted comments. So I have created a Post class that groups all these items.
Setting up a DTO
In my \Classes\ namespace, I've created a new Post class. First, we define the properties we will need. Then we add the getters and setters for each of them. Finally, we set up the constructor, which will call each of the classes we need in the view.
namespace Classes;
use DateTime;
class Post
{
private int $pk_id;
private string $title;
private string $excerpt;
private string $content;
private DateTime $created_at;
private DateTime $updated_at;
private int $author_id;
private string $author_firstname;
private array $comments;
public function __construct(Article $article, User $author, array $comments)
{
$this->setPkId($article->getId());
$this->setTitle($article->getTitle());
$this->setExcerpt($article->getExcerpt());
$this->setContent($article->getContent());
$this->setCreatedAt($article->getCreatedAt());
$this->setUpdatedAt($article->getUpdatedAt());
$this->setAuthorId($article->getAuthorId());
$this->setAuthorFirstname($author->getFirstname());
$this->setComments($comments);
}
/**
* #param int $pk_id
*/
public function setPkId(int $pk_id): void
{
$this->pk_id = $pk_id;
}
/**
* #return int
*/
public function getPkId(): int
{
return $this->pk_id;
}
// (etc)
}
We now need to update the ArticleController, which should no longer pass the Article, Comment and User objects, but only the new Post object.
namespace Controllers;
class ArticleController extends Controller
{
// (...)
/**
* Get an article and display it
*
* #return void
*/
public function show(): void
{
// (...)
// Find Article :
$article = $this->articleModel->find($article_id);
if (!$article) {
Http::error404();
}
// Find Comments :
$commentaires = $this->commentModel->findAllByArticle($article_id);
// Find User (author)
$user = $this->userModel->find($article->getAuthorId());
// Data Transfert Object instance :
$post = new Post($article, $user, $commentaires);
$pageTitle = $post->getTitle();
// Pass DTO to view :
Renderer::render('articles/show', compact('pageTitle', 'post'));
}
}
We just need to update our view to use the new Post object and it's done ! Thanks to #daremachine for his help :)

Related

Relations inside DTO and php7.4 : how to hydrate?

I wonder what is the good practices :
Let's say I have 2 entities, ManyToOne. Both are ApiResources, and both have an Output DTO. So Both have a transformer.
<?php
/**
* #ORM\Entity
* #ApiResource(
* output="Dto\Foo"
* )
*/
class Foo
{
private int $id;
/**
* #ORM\ManyToOne(targetEntity="Bar")
*/
private Bar $bar;
}
Problem is, when I transform the entity Foo into a DTO Foo, I want to hydrate it with a Bar DTO, not a Bar entity. But since I hydrate it with from an entity, I have a Bar entity. Later in the process, the Bar entity is replaced by a Bar DTO, ApiPlateform is working, but my mental problem is : the bar property type is modified over time (Moreover it can't be Typehinted). Seems dirty to me, isn't it ?
Illustration:
the Transofmer
<?php
use ApiPlatform\Core\DataTransformer\DataTransformerInterface;
class FooEntityToFooDToTransormer implements DataTransformerInterface
{
public function transform($object, string $to, array $context = [])
{
return new FooDto($object);
// maybe there is a better way to hydrate FooDto, by getting directly a BarDto here ?
}
}
The DTO :
<?php
namespace Dto;
class Foo
{
public int $id;
// problem is I cant typehint here
public $bar;
public function __construct(FooEntity $fooEntity)
{
$this->id = $fooEntity->getId();
$this->bar = $fooEntity->getBar(); // <-- return a Bar entity, transformed later by ApiPlatform into a Bar DTO.
}
}
It there a way or a good practice to proper hydrate a DTO from an entity, especially about relations ?
Edit :
I actually prefer not Typehint $bar as its normalization (so its transformation) should be handled by ApiPlateform. But then, circular references are not handeled (memory limit) and I don't really know why (probably confusion between object and DTO).
I don't think my FooTransformer should know how to transform Bar, because according to the context I could need one transformer or another, or an IRI... Test all of them with "supportTransformation" and so, for every relation ? And what about circular ?
It's a little mess, my solution for now is to choose to return FooDto (without transform barDto) or to return an IRI, according to the context (which I am absolutly not sure of what I'm doing with it due to the lack of documentation about $context).
Same for BarTransformer.
So every transformer need to choose to actually transform the object without handeling transformation of relations, or return the correspondant IRI. That is the less dirty I found.
I guess you have two possible solutions here, to extend your DTO's constructor signature by one more argument and adjust your transformer or to do the transformation right inside your DTO's constructor:
<?php
namespace Dto;
class FooDto
{
public BarDto $bar;
// first variant
public function __construct(FooEntity $fooEntity, BarDto $barDto)
{
$this->id = $fooEntity->getId();
$this->bar = $barDto;
}
// second
public function __construct(FooEntity $fooEntity)
{
$this->id = $fooEntity->getId();
$this->bar = new BarDto($fooEntity->getBar());
}
}

symfony/doctrine: cannot use object without refreshing

It's the first time I run into this problem. I want to create a doctrine object and pass it along without having to flush it.
Right after it's creation, I can display some value in the object, but I can't access nested object:
$em->persist($filter);
print_r($filter->getDescription() . "\n");
print_r(count($filter->getAssetClasses()));
die;
I get:
filter description -- 0
(I should have 19 assetClass)
If I flush $filter, i still have the same issue (why oh why !)
The solution is to refresh it:
$em->persist($filter);
$em->flush();
$em->refresh($filter);
print_r($filter->getDescription() . " -- ");
print_r(count($filter->getAssetClasses()));
die;
I get:
filter description -- 19
unfortunately, you can't refresh without flushing.
On my entities, I've got the following:
in class Filter:
public function __construct()
{
$this->filterAssetClasses = new ArrayCollection();
$this->assetClasses = new ArrayCollection();
}
/**
* #var Collection
*
* #ORM\OneToMany(targetEntity="FilterAssetClass", mappedBy="filterAssetClasses", cascade={"persist"})
*/
private $filterAssetClasses;
public function addFilterAssetClass(\App\CoreBundle\Entity\FilterAssetClass $filterAssetClass)
{
$this->filterAssetClasses[] = $filterAssetClass;
$filterAssetClass->setFilter($this);
return $this;
}
in class FilterAssetClass:
/**
* #var Filter
*
* #ORM\ManyToOne(targetEntity="App\CoreBundle\Entity\Filter", inversedBy="filterAssetClasses")
*/
private $filter;
/**
* #var Filter
*
* #ORM\ManyToOne(targetEntity="AssetClass")
*/
private $assetClass;
public function setFilter(\App\CoreBundle\Entity\Filter $filter)
{
$this->filter = $filter;
return $this;
}
Someone else did write the code for the entities, and i'm a bit lost. I'm not a Doctrine expert, so if someone could point me in the good direction, that would be awesome.
Julien
but I can't access nested object
Did you set those assetClasses in the first place?
When you work with objects in memory (before persist), you can add and set all nested objects, and use those while still in memory.
My guess is that you believe that you need to store objects to database in order for them to get their IDs assigned.
IMHO, that is a bad practice and often causes problems. You can use ramsey/uuid library instead, and set IDs in Entity constructor:
public function __construct() {
$this->id = Uuid::uuid4();
}
A database should be used only as a means for storing data. No business logic should be there.
I would recommend this video on Doctrine good practices, and about the above mentioned stuff.
Your problem is not related to doctrine nor the persist/flush/refresh sequence; the problem you describe is only a symptom of bad code. As others have suggested, you should not be relying on the database to get at your data model. You should be able to get what you are after entirely without using the database; the database only stores the data when you are done with it.
Your Filter class should include some code that manages this:
// Filter
public function __contsruct()
{
$this->filterAssetClasses = new ArrayCollection();
}
/**
* #ORM\OneToMany(targetEntity="FilterAssetClass", mappedBy="filterAssetClasses", cascade={"persist"})
*/
private $filterAssetClasses;
public function addFilterAssetClass(FilterAssetClass $class)
{
// assuming you don't want duplicates...
if ($this->filterAssetClasses->contains($class) {
return;
}
$this->filterAssetClasses[] = $class;
// you also need to set the owning side of this relationship
// for later persistence in the db
// Of course you'll need to create the referenced function in your
// FilterAssetClass entity
$class->addFilter($this);
}
You may have all of this already, but you didn't show enough of your code to know. Note that you should probably not have a function setFilterAssetClass() in your Filter entity.

Saving Related Entities Doctrine/Symfony2

I have one specific issue. I have two entities:
class MyPlaylist {
...
/**
* #var Array
* #ORM\OneToMany(targetEntity="MyPlaylistContent", mappedBy="myPlaylist", orphanRemoval=true)
* #ORM\OrderBy({"position" = "DESC"})
*/
private $myPlaylistItems;
and
class MyPlaylistContent {
....
/**
* #ORM\ManyToOne(targetEntity="MyPlaylist", inversedBy="myPlaylistItems")
*/
private $myPlaylist;
Now I have this in my service
....
$myPlaylist = new MyPlaylist();
$myPlaylist->setUser($user);
$myPlaylist->setActive(true);
// add tracks
foreach ($playlist->getMyPlaylistItems() as $item) {
$entity = new MyPlaylistContent();
$entity->setTrack($item->getTrack());
$entity->setMyPlaylist($myPlaylist);
$this->em->persist($entity);
}
$this->em->persist($myPlaylist);
$this->em->flush();
\Doctrine\Common\Util\Debug::dump($myPlaylist);
return $myPlaylist;
so, I return a new playlist. If I look at the database, all works fine. I have both entities and in MyPlaylistContent - 3 tracks. But
\Doctrine\Common\Util\Debug::dump($myPlaylist); shows next
["active"]=> bool(true) ["myPlaylistItems"]=> array(0) { }
On the page, the app shows the empty playlist (no tracks). If I refresh the page, I can see all tracks.
The point is, if you open the page, the controller will call the service, build the content and return the list as a response.
It looks as the same example, but it does not work for me
http://symfony.com/doc/current/book/doctrine.html#saving-related-entities
What is wrong here? Why don't I get tracks for the current entity?
You forget to add MyPlaylistContent to MyPlaylist.
Use this snippet into foreach
$myPlaylist->addMyPlaylistContent($myPlaylistContent);
Of course change name or implement method accordingly
First note: this is because objects are "normal" php objects, they have nothing to do with doctrine so, relationships are only a doctrine concept. EntityManager in doctrine will handle this kind of processes, not php itself. If you take a look to your classes methods you will probably notice that no "connection" (assignments) are made between those objects. If you would like, you can modify MyPlaylistContent to add itself to MyPlaylist once assigned.
Something like
class MyPlaylistContent
{
[...]
public function setMyPlaylist(MyPlaylist $mp)
{
$this->myPlaylist = $myPlaylist;
$mp->addMyPlaylistContent($this);
return $this;
}
Second note: hope your names are more consistents of these ones :)

Removing OneToMany elements, Doctrine2

I've got this model;
Itinerary, Venue, ItineraryVenue.
I needed many to many relation between itineraries and venues but also I wanted to store some specific data about the relation (say notes, own photo, etc.), so I decided to introduce a new entity named ItineraryVenue.
So Itinerary has collection of ItineraryVenues which in turn, refer to Venues.
My problem is that I can't remove ItineraryVenue from a Itinerary object.
$itinerary->itineraryVenues->removeElement($itineraryVenue);
$em->flush();
removes element from the php collection, but doesn't remove this $itineraryVenue from database.
I've managed to force Doctrine2 to remove $itineraryVenue, but only when I annotate the Itinerary::$itineraryVenues with orphanRemoval=true.
Since orphan removal treats Venue as a private property it also removes Venue entity, I don't want that.
Is there an relation configuration option or is removing "by hand" the olny way to make it work as I want?
Hard to believe it, it's a common relation pattern.
Entities definitions:
class Itinerary
{
/**
* #ORM\OneToMany(targetEntity="ItineraryVenue", mappedBy="itinerary", cascade={"persist", "remove"})
*/
private $itineraryVenues;
function __construct()
{
$this->itineraryVenues = new ArrayCollection();
}
}
class ItineraryVenue
{
/**
* #ORM\ManyToOne(targetEntity="Itinerary", inversedBy="itineraryVenues")
*/
private $itinerary;
/**
* #ORM\ManyToOne(targetEntity="Venue")
*/
private $venue;
function __construct()
{
}
}
class Venue
{
}
You are doing things right: orphanRemoval - is what you need. So, you should override default Itinerary::removeItineraryVenue like
public function removeItineraryVenue(\AppBundle\Entity\ItineraryVenue $itineraryVenue)
{
$itineraryVenue->setItinerary(null);
$this->itineraryVenues->removeElement($itineraryVenue);
}
The full working example is here https://github.com/kaduev13/removing-onetomany-elements-doctrine2.

Advice on PHP OOP code

In a PHP project I have three layers (Logical)
UI :
Place to store UI php pages. ex: ManageUsers.php
Code :
All Entity Classes Related to system. ex: User.php
DataAccess :
Data Access Classes for Entity Classes ex: UserDataAccess.php
So to add user from UI, I create a instances of User class set properties of it & call AddUser method (in user class). In AddUser method it creates a instance of UserDataAccess class & pass the same ($this) object to UserDataAccess Insert function. It gets the data from User object properties & updates the table. (returns status back to UI through Code layer)
I repeat this for other functions like Edit & Delete.
Is this the preferable OOP way to achive this CRUD functions or can I reduce one layer & combine Code & DataAccess layer to make it more simple?
Is it ok to instanciate User class more than once in ManageUser page (one instance for Add, one for Edit and so on) or should instanciate it only once for a single page?
Sorry about the length & complexity of the question.. Thanks in advance...
Is this the preferable OOP way to achive this CRUD functions or can I reduce one layer & combine Code & DataAccess layer to make it more simple?
The 3 layer method (Interface "U.I.", Business logic "Code" & Data Access), is the suggested way to do things.
You'll find that there are several business apps. that use the "2 layer" method that you suggested. If your think that application is going to "grow", you should use the 3 layer instead.
But, have in mind, that its complexity will also increase.
You may also want to get (download, develop, buy) a separate tool program that reads a database or data access, and generates your entity classes from it, instead of programming all by yourself.
If possible, do the same, for the "Code" or "Business logic" entities.
One more thing, avoid putting all you entities, in a single file.
Use a file per entity, instead. Make, unleast, a single "GUI" folder for your interface or "boundary" classes,
a folder for your "Code" classes, and a folder for your "Data Access" classes.
Your website or application may be small, and this suggestions may seen very complex nad unnecesary.
But, by experience, apps. tend to grow, even "pet" or "personal" websites, unexpectly may grow, and become complex.
Even, if its a personal, small project, you may try to use the suggestions, in order to be prepare for large websites or apps.
Additional Suggestion
One trick I learn from a coworker, to make code more maintainable, its to use a program that generates the entities code, automatically, and, later, not make instances of those class entities, as usually done in the 3 layer method.
Instead, a second class entity was code, usually manually, adding some members or features, that may be updated regularly.
So the first (parent) class have the members that can be obtained from the database (or XML file), and the second (child) class, has the members that may not be obtained from the database, and also require to do a lot of changes, that require to be "coded by hand".
Example:
(1) You have a database table (or XML file or local file) like this:
CREATE TABLE User
(
integer UserKey primary key,
varchar(20) UserAlias,
varchar(250) UserPasword,
integer UserIsActive, -- My D.B. doesn't support boolean
)
(2) You generate a Data-Access-Layer entity like this, wheter manually or tool-generated:
abstract class FieldsUserDataAccessEntityClass
{
/* integer */ protected $fieldUserKey = 0;
/* string */ protected $fieldUserAlias = "";
/* string */ protected $fieldUserPassword = "";
/* integer */ protected $fieldUserIsActive = 0;
/* integer */ public function CalculateNewKey();
/* void */ public function Insert();
/* void */ public function InsertWithKey(/* integer */ $newKey);
/* void */ public function Update();
/* void */ public function Delete();
} // class
This previous class is commonly used directly (without the "abstract" feature).
But, the suggestiuon is, that in other file, this, (I skip the methods code):
/* concrete */ class UserDataAccessEntityClass extends FieldsUserDataAccessEntityClass
{
public /* integer */ function getUserKey();
public function setUserKey(/* integer */ $newValue);
public /* string */ function getUserAlias();
public function setUserAlias(/* string */ $newValue);
public /* string */ function getUserPassword();
public function setUserPassword(/* string */ $newValue);
public /* string */ function getUserPassword();
public function setUserPassword(/* string */ $newValue);
public /* bool */ function getUserIsActive();
public function setUserIsActive(/* bool */ $newValue);
} // class
...
/* void */ function GlobalDoSomething()
{
/* UserDataAccessEntityClass */ $CurrentUser = new UserDataAccessEntityClass();
/* integer */ newKey = $CurrentUser->CalculateNewKey();
$CurrentUser->setUserKey(newKey);
$CurrentUser->setUserName("JohnDoe");
$CurrentUser->setUserPassword("a9b8c7d6e5f4");
$CurrentUser->setUserIsActive(true);
$CurrentUser->Insert();
$CurrentUser = null;
} //
...
I use the casting of the "IsActive" field from "integer" to "boolean", as an example,
could be other stuff like truncating a string that is too long.
Some developers will complain, and suggest to do this in the business logic entity.
(3) You generate a Business-Logic-Layer entity like this, wheter manually or tool-generated:
abstract class FieldsUserLogicAccessEntityClass
{
/* integer */ protected $fieldUserKey = 0;
/* string */ protected $fieldUserAlias = "";
/* string */ protected $fieldUserPassword = "";
/* integer */ protected $fieldUserIsActive = 0;
/* void */ public function DoSomeThing();
} // class
In other file, this, (I skip the methods code):
class UserLogicAccessEntityClass extends FieldsUserLogicAccessEntityClass
{
public /* integer */ function getUserKey();
public function setUserKey(/* integer */ $newValue);
public /* string */ function getUserAlias();
public function setUserAlias(/* string */ $newValue);
public /* string */ function getUserPassword();
public function setUserPassword(/* string */ $newValue);
public /* string */ function getUserPassword();
public function setUserPassword(/* string */ $newValue);
public /* bool */ function getUserIsActive();
public function setUserIsActive(/* bool */ $newValue);
/* void */ public function DoAnotherThing();
} // class
...
/* void */ function GlobalDoSomething()
{
/* UserLogicAccessEntityClass */ $CurrentUser = new UserLogicAccessEntityClass();
$CurrentUser->setUserName("JohnDoe");
$CurrentUser->setUserPassword("a9b8c7d6e5f4");
if ($CurrentUser->TryLogin())
{
echo "Welcome !!!";
}
else
{
echo "Security Alert !!!";
}
$CurrentUser = null;
} //
...
These method helps a lot when, you have to add or remove or update fields in the database, but, its more complex, so a entity generator is required.
The idea its that a tool generates the "abstract with fields" entity classes,
and you modify manually the "concrete child" entity classes, when required.
And in real world programming, modifying hte entities manually, happens a lot.
Is it ok to instanciate User class more than once in ManageUser page (one instance for Add, one for Edit and so on) or should instanciate it only once for a single page?
Its ok, to instantiate an entity class, several times, but, its more common, to have them as temporally objects int methods. In case of objects like the "currently logged user", you may use a "singleton" object.

Categories