Data + behaviour = object. Data + view = …projection?

  • This topic has 5 replies, 1 voice, and was last updated 2 months ago by TorbenKoehn.
Viewing 6 posts - 1 through 6 (of 6 total)
  • Author
    Posts
  • #2266
    usernameqwerty004
    Participant

    Should web dev have a language construct for projections of data into a view? Similar like an object is a tuple of data and behaviour, a projection would be a tuple of data and a view (HTML, viewfile, Twig, …). With “data”, I mean here a data-value object (or data-transfer object) – an immutable object with only properties, no methods (except getters). What’s the best way to map a DVO unto a view? Or a list of DVOs.

    #2267
    zmitic
    Guest

    If I understand correctly, you would prefer DTO between entity and view? I.e. not to directly assign entities to templates; is that right?

    If so, I recently started playing with this idea. Most simple one, edited a bit to get an idea:

    I have an entity PostalCode with just a name and ID. There is no other field like how many addresses belong to it.

    But in order to display it in template, I have DTO like this (I used View suffix):

    “`php
    class PostalCodeView
    {
    public string $id;
    public string $name;
    private LazyValue $nrOfAddresses;

    /**
    * @param LazyValue<int> $nrOfAddresses
    */
    public function __construct(PostalCode $postalCode, LazyValue $nrOfAddresses)
    {
    $this->id = (string)$postalCode->getId();
    $this->name = $postalCode->getCode();
    $this->nrOfAddresses = $nrOfAddresses;
    }

    public function getNrOfAddresses(): int
    {
    return $this->nrOfAddresses->getValue();
    }
    }
    “`

    The key thing here is **second** parameter in constructor. Factory that creates this View is:

    “`php
    /**
    * @extends AbstractViewFactory<PostalCode, PostalCodeView>
    */
    class PostalCodeViewFactory extends AbstractViewFactory
    {
    private AddressRepository $systemAddressRepository;

    public function __construct(AddressRepository $systemAddressRepository)
    {
    $this->systemAddressRepository = $systemAddressRepository;
    }

    public function one($entity): PostalCodeView
    {
    $lazy = new LazyValue(fn() => $this->nrOfAddresses($entity));

    return new PostalCodeView($entity, $lazy);
    }

    private function nrOfAddresses(PostalCode $postalCode): int
    {
    // some expensive COUNT() query
    return 42;
    }
    }
    “`

    and I assign view class to template, not entity:

    “`twig
    {{ view.nrOfAddresses }}
    “`

    Now here is the trick; if I **don’t** display nrOfAddresses in my template, then this expensive COUNT query will **not** be executed. Or if I need it twice, query will be executed only once because of my LazyValue class: https://gist.github.com/zmitic/63f228df737c091697536e27d7010443

    Why I did this?

    I didn’t want to pollute my entity with too many methods which will not be used anywhere but in templates alone. And I avoid bidirectional relations; not just for performance reasons but also that it would require more code.

    Secondly; I set psalm to ignore “PossiblyUnusedMethod, UnusedMethodCall, UnusedProperty“ for my View files. But entities are still under full control; if there is a method unused, I will know. And they are more important.

    Third; these view property and methods are really named better for templates. For example, some template needs all the parts of my Address.

    So AddressView has “sprintf“ or what is needed for template; no need to put something like that in entity where is serves no purpose in business logic:

    “`
    $this->longName = sprintf(‘%s %s %s’,
    $address->getNumber(),
    $street->getName(),
    $address->getStreetType(),
    );
    “`

    In reality, this is far more complicated than just “nrOfAddresses“ or “longName“. I have some really complicated application and this idea of lazy loading came pretty handy.

    Is this something you want? Or anyone have idea how to improve this?

    #2268
    adrianmiu
    Guest

    You can create wrappers around your objects

    class UserViewModel {

    static function wrap(UserModel $user) {
    return new static($user);
    }

    public function getFullName() {
    return $this->user->getFirstName() . ‘ ‘ . $this->user->getLastName();
    }

    }

    The view models can be more complex (eg: depend on a URL builder) and it will be up to you to decide how to handle this.

    #2269
    tored950
    Guest

    Risk of a data layer between the controller and the view, the DVO as you call it, is that it creates just one more level of bureaucracy .

    The desire of introducing that layer usually stems from that the project is using an active record ORM and therefore you want to reduce the mixup between model layer knowledge and view layer knowledge.

    In that case the problem is the ORM and not the view.

    I follow the KISS principle so I handwrite all my SQL and create custom data classes for almost every SQL SELECT query. My data classes has only properties. It can have methods if it only operates on the data on the object and if it loosely classified as a utility function. By doing like this I never feel the urge to create yet another layer of data classes.

    Short example

    final class User
    {
    public int $user_id;
    public string $email;
    public string $firstname;
    public string $lastname;

    public function getFullname(): string
    {
    return “{$this->firstname} {$this->lastname}”;
    }
    }

    final class UserInfo
    {
    public int $user_id;
    public string $email;
    }

    final class UserRepository
    {
    private $database;

    public function __construct(Database $database)
    {
    $this->database = $database;
    }

    public function getUserByUserId(int $user_id): ?User
    {
    $row = $this->database->selectRow(
    “SELECT user_id, email, firstname, lastname
    FROM user
    WHERE user_id = ?
    “, [$user_id]);

    if ($row === null) {
    return null;
    }
    $user = new User();
    $user->user_id = (int) $row[‘user_id’];
    $user->email = $row[’email’];
    $user->firstname = $row[‘firstname’];
    $user->lastname = $row[‘lastname’];
    return $user;
    }

    public function getUserInfoByUserId(int $user_id): ?UserInfo
    {
    $row = $this->database->selectRow(
    “SELECT user_id, email
    FROM user
    WHERE user_id = ?
    “, [$user_id]);

    if ($row === null) {
    return null;
    }
    $user = new UserInfo();
    $user->user_id = (int) $row[‘user_id’];
    $user->email = $row[’email’];
    return $user;
    }

    public function updateEmailByUserId(int $user_id, string $email): void
    {
    $this->database->updateRow(
    “UPDATE user
    SET email = ?
    WHERE user_id = ?;”, [$email, $user_id]);
    }
    }

    Here I have two different user classes, one that has all information User and one smaller UserInfo. User has a utility method to get the fullname. Sometimes you don’t need all information about an entity, so you should only fetch what you need, that is what UserInfo could be used to.

    Now this example is very simplistic, but if you imagine that there are some SQL JOINS in these queries it makes more sense. Remember the view doesn’t care if email is stored in the same table as the user, it could be but doesn’t need to. By hiding that behind a repository method and custom data class, the consumer of that method doesn’t need to know. He just wants the data in a simple package.

    I also have a method for updating an email for a user, I usually never send in an entire data object in those cases, only the id and the value I want to change. Otherwise it is a risk that the user of the repository method doesn’t know what data the method reads and what it ignores of that data object. Always better to be specific as possible.

    User and UserInfo can be directly used by any view and you get nice types to help you code in your view and they have only properties and simple methods to help you with that. Convention is read only but because I don’t accept a User or UserInfo for writing in my UserRepository it doesn’t really matter because it will not happen, thus I don’t need immutability to guard myself against weird ORM flushes.

    I think this follows the KISS principle quite well. It is small, tidy and uses normal PHP language constructs, anyone reading it understands what is happening and they don’t need to unserstand a complex ORM layer or a complex view layer.

    These data types can be reused over the entire project and if I need to another projection of User I create that when needed, for example I need the password hash when the user logs in (usually the only time you need it in your project), I then have class maybe called UserInfoWithPassword. That class can have a helper method that compares hashes.

    #2270
    Crell
    Guest

    What you describe is called MVVC, I believe. It’s harder to google for because MVC has all the google juice, but there’s a considerable amount of data out there about it already. (I think mostly C# targeted, but you can easily translate that to PHP speak.)

    Worth looking into.

    #2271
    TorbenKoehn
    Guest

    If you want to use proper dependency injection when constructing your view data, I am using the following pattern quite successfully with Symfony.

    As an example, I am translating enumeration values and provide the translation directly in my output DTOs. Or I am generating URLs fitting for the current request host, as an example.

    – AppEntityUser (The Entity)
    – AppDtoUserUserOutput (The “View”, getters only)
    – AppDtoUserUserOutputFactory (+Interface) (The “View” factory)

    Then, whenever you output a User-entity to a consumer of your app, use the OutputFactory (this includes HTTP responses, but also SSE/WS messages and most probably even your ElasticSearch transforms (in most cases your Output DTO _is_ the ElasticSearch transform)

    return $this->userOutputFactory->createFromEntity($user)

    The factory-classes can provide context to the DTO and add information based on requests, current user or environment, as an example.

    I use JMS Serializer and a variation of custom annotations to serialize these output DTOs for my client (I want to switch to API Platform soon)

    I do the same with XyzInput DTOs, which also eases up validation and separates my user and my entities very strictly.

    You can nest this pattern, e.g. if your User has an Address, your UserOutputFactory might use the AddressOutputFactory to place an address output in the user and not an address entity directly.

    It’s also really framework-agnostic, obviously, it’s simply a DTO and a factory for it. It’s easy af and gives you full control over every aspect of your output, regardless of what kind of context you require (DI got your back)

    It also has a very low complexity, there is no magic involved, it’s strict, manual mapping. It can get tedious, though, especially with big output DTOs and many fields to map.

Viewing 6 posts - 1 through 6 (of 6 total)
  • You must be logged in to reply to this topic.