Ipeleng Molete
The Home of Ipelatech's Blog

The Home of Ipelatech's Blog

Ipela-Shepherd

Ipela-Shepherd

A fresh take on DTOs

Ipeleng Molete's photo
Ipeleng Molete
·Nov 23, 2021·

6 min read

Table of contents

  • Introduction
  • Features
  • Installation
  • Concepts
  • Usage

Introduction

Shepherd, the package, not the System represents the evolution of how I think about development. I built an entire System about how to write good code around this library, however it isn't necessary to use this library to impement it. You can download it here.

The pain

A while back I was working on a client project and as I added more features and the logic starting becoming more complex, I started getting really frustrated at how difficult it was for me to track the flow of data in my app. It seemed like data came in from the front-end, somehow moved through the logic and ended up in the database. Over time, it became apparent that something had to be done differently, and I started to look for solutions to my problem.

"There must be a better way! - Me (actual quote)

Cut to the montage with me looking for a solution by watching talks, reading articles and tutorials and finally having a breakthrough to a banging 80s soundtrack

First steps

And we reach the first version of Shepherd. I got here by answering the question, "How do others do it?" Let's start with a diagram of how data flows through an app. The example is for MVC frameworks but really applies to any kind of app with similar moving pieces.

1.svg

As you can see, the "C" level seems to be where things go awry. To put it another way, if I had to look at your controller code right now, is this what it would communicate? How straight/long is the execution path of the code? Is it easy to trace? To make the rest of this section make sense, let's redefine the other two layers. Our "V" layer, I'm going to only look at the HTTP protocol - which controls how data makes it to the backend, and the "M" layer will represent the database.

Now that that's sorted, we can start seeing how others have solved the problem I was having. If you look at HTTP and Databases/SQL, they are nothing fancier than processors of data. They take data in a certain format, do something with it and then do something else with the output. What they do tells us something about what actually happens to data in an app. What is boils down to is, we can view data, create new data, edit/change/update and delete it. Sounds like CRUD, hey? (hint: it is!).

To do that, HTTP gives us REST and GET, POST, PUT/PATCH and DELETE. Here's some data, here's what must be done with it. It doesn't care about the data is, just as long as it's in the right format. SQL gives us SELECT, INSERT, UPDATE and DELETE. Here's some data, here's what must be done with it. It doesn't care about the data is, just as long as it's in the right format.

And us? How do we write our service layer code?

class Customer {
    public method feed_cat() {
        //save meal log to db
    }
}

Let's look at the data flow now shall we?

2.svg

This should start ringing bells and it did for me. I realised I could constrain the API of my service layer to align with the HTTP and Database and this the first version of Shepherd. There were two types of classes you could create, I called ShepherdDataObjects (which were just regular DTOs) and ShepherdDataProcessingObjects. It was a class representing a domain object with data and only 5 behaviours.

class Customer {
    protected string $name;
    protected string $email;

    public function retrieve_all() {}
    public function retrieve() {}
    public function initialise() {}
    public function modify() {}
    public function remove() {}
}

The names were chosen so as not to clash with common method names. As any good dev would do, having found The Ultimate Solution, I immediately set out putting everything in DataProcessingObjects. Turns out this initial version had a "balance" problem. For simple operations, like retrieving all records from a database, it was overkill to spin up a new object, for more complex situations, where several DataProcessingObjects have to interact with each other, the code would often come out even uglier than it went in!

"There must be a betterer way! - Me (more actual quote)

There was.

The Promised (Code)Land

The initial version defeated two enemies I had at the time, constructors and mutability. I like some of the concepts of Functional Programming, I don't see myself committing to it full-time, however there's a lot of power in the ideas it puts forward - immutability is one of them. The final piece of the puzzle came together when I made the ground-breaking realisation that data doesn't have behavour (I know, right?).

Which leads us to the current and feature-complete version of the libary.

I got rid of the DataProcessingObject and just had a pure(ish) immutable DTO. The Constructor Problem™ was solved by creating a "constructor class". You see the initialise() method above? It serves as a static constructor, where you pass in the initial values you want and it spits back a DataObject. The problem is in PHP, you can't overload methods (easily) and one of the problems I had was that sometimes I needed to initialise different members differently.

The solution was I created a Base Class that would handle the initialisation logic, and if I ever needed to a different way to initialise, I'd subclass it, override the initialise method and be on my merry way. This also had the spin-off effect of separating data from logic (which is a core tenet in the Shepherd Philosophy!). In this way, I could create very small, focussed classes that were easy to test. The boilerplate and API isn't too bad, the benefits gained far exceed the few extra lines of code.

Features

Ability to create and modify immutable DTOs.

Installation

Install with composer

  composer require ipelatech/ipela-shepherd

Concepts

IShepherdDataObject

An abstract class you sublcass to create your "DTO". Only use protected typed members in here.

  • Methods
    • public function has(string $trait_name): bool
      Check if there is a trait (use fully qualified name, i.e. with namespace)
    • public function to_array() : array
      Convert IShepherdDataObject to array
    • public static function from_array(array $parameters) : IShepherdDataObject
      Create IShepherdDataObject from array
    • public function set(string $name, $value) : IShepherdDataObject
      Modify variable $name in the ISheperherdDataObject
    • public function __toString() Prints string of IShepherdDataObject, usage: $object->toString()
    • public function __get($name) Allows you to get the value of on of the protected variables in the class

IShepherdHandlerInitialiser

  • Methods
    • protected function populate_class(string $class_name, array $parameters)
    • public function initialise() : ShepherdDataObject
      You need to implement this if you subclass the Initialiser

IShepherdHandlerModifier

  • Methods
    • public function modify() : ShepherdDataObject
      You need to implement this if you subclass the Initialiser

ShepherdDataObjectInitialiser

  • Methods:
    • public static function initialise(IShepherdDataObject $object_to_initialise, array $parameters = []) : IShepherdDataObject

ShepherdDataObjectModifier

  • Methods:
    • public static function modify(IShepherdDataObject $object_to_modify, array $parameters) : IShepherdDataObject

Usage

The generalised workflow for working with ShepherdDataObjects is:

  1. Create a DTO and extend it with the IShepherdDataObject abstract class.
  2. Use the ShepherdDataObjectInitialiser or create an initialiser class that extends IShepherHandlerInitialiser
  3. Instanstiate and use the ShepherdDataObject
  4. Use the ShepherdDataObjectModifier or create a class that extends IShepherdHandlerModifier to modify values, or use the set() convenience method

The rest is in the documentation.

Conclusion

Like I said earlier, to implement the Shepherd System, you don't need to use the package. For me the power of it comes from the process I went through creating it. Along the way, I've picked up so many useful habits that I use in my all my coding, whether it's for money or fun. Also, it's very handy when I do need a DTO... so wins all round!

I hope you can extract the same value, at least from the thinking, as I have.

Thanks for dropping by. Le saleng hanthle!

Cover Image

 
Share this