David NĂ©grier CTO

Today,

I'm really happy to share with you the release of TDBM 5.1.

TDBM? What is that?

TDBM is a PHP ORM. It is actually a database first ORM. TDBM understands your database model and generates PHP classes for you to access your database.

If you have never heard of TDBM before, a good starting point is my article about the release of TDBM 5 or you can directly jump to the documentation to learn more.

So... What's new in 5.1?

Annotations! All the way!

You know that the corner-stone of TDBM philosophy is that your data-model is meaningful and that by looking at your datamodel, we can infer your domain model.

So, in TDBM, your data-model is your starting point. You design the DB first and PHP classes are generated from it.

This hypothesis holds true for simple to moderately complex data model. TDBM does a pretty good job at understanding your data model. It relies on constraints, it can detect pivot tables, indexes, etc... But there are things TDBM cannot assess by itself.

TDBM 5.1 brings the notion of annotations. By annotating your data-model, you can help TDBM understand your DB model even better than it did before!

Please note: you put annotations on your data-model (in the columns and tables comments). Not in your PHP code!

The TDBM 5.1 release also sports a complete revamp of the PHP files generation. The process was mostly done using templates and it has been rewritten to use the Zend-Code generation library. As a benefit, you can now write plugins that will intercept code generation and modify the code generated, on-the-fly. We will see later what practical use we have for this.

Finally, TDBM 5.1 comes with numerous improvements to the data model detection (we handle way better corner cases) and to performance.

Getting rid of the anemic data model

ORMs that follow the "Active record" pattern suffer from one common issue. Each row is represented by an object. The object usually has a getter and a setter for each column. So the object is directly exposing its inner state to the outside world.

But as Uncle Bob Martin puts it, this is an anti-pattern. The very point of object oriented design is to hide the inner state of objects and expose only the relevant parts through a set of well written methods. If any property is accessible using a getter and modifiable using a setter, there is no real point in having an object.

In TDBM, since version 3, you can easily add methods to your beans in order to add some behaviour. This is why your beans are split in 2 classes. For instance: User and AbstractUser.

Let's assume you have a users table with columns status and enable_date. Say a used can be "enabled", "pending" or "disabled".

In order to enable a user, in TDBM 5, you could write something like:

$user->setStatus('enabled');
$user->setEnableDate(new DateTimeImmutable());

But of course, the better way is to encapsulate the "enable" behaviour in the object itself.

class User extends AbstractUser
{
    public function enable(): void
    {
        if ($this->getStatus() === 'enabled') {
            throw new InvalidStatusChangeException('User is already enabled');
        }
        $this->setStatus('enabled');
        $this->setEnableDate(new DateTimeImmutable());
    }
}

You can already do the above with TDBM 5.0. But the problem is that even if you can add new methods, the setStatus and setEnableDate are still exposed to the outside world. It was impossible tohide the setStatus and setEnableDate methods.

Until TDBM 5.1!

Thanks to the new @ProtectedGetter and @ProtectedSetter annotations, you can now tell TDBM to hide some getters and setter from the generated data-model.

CREATE TABLE `users` (
  `id` INTEGER NOT NULL,
  `login` varchar(255),
  `password` varchar(255) COMMENT '@ProtectedGetter',
  `status` varchar(50) COMMENT '@ProtectedSetter',
  `enable_date` DATETIME COMMENT '@ProtectedSetter',
  PRIMARY KEY (`id`)
);

Note that the methods are still generated, but they are now protected by default, so they are only accessible from within the object (or one of its children).

These 2 annotations were the missing items that allow you to turn your beans into proper domain objects.

JSON serialization hints

This feature brought to you by @brain-diminished. Thanks!

TDBM 5 does its best to offer a default JSON serialization that works out of the box. It serializes all columns, and it will serialize many-to-one and many-to-many relationships one level down the tree by default.

But there are plenty of cases where you might want something different. You may want to prevent some columns from being serialized. Or you may want to serialize a one-to-many relationship.

For those cases, until now, you had to override the jsonSerialize method and code your own.

Starting with TDBM 5.1, there is another way. You can now annotate DB columns with @JSONxxx annotations and TDBM will generate a proper JSON serialization for you.

Here is a list of available annotations:

  • @JsonKey: Change the name of the serialized property
  • @JsonFormat: Specify a format when serializing (for date formats or floating point number formats)
  • @JsonIgnore: To hide a property
  • @JsonInclude: Put this on a foreign-key to force Json serialization to serialize the object pointed by the foreign-key.
  • @JsonRecursive: Put this annotation to set parameter $stopRecursion to true when calling jsonSerialize on sub-object
  • @JsonCollection: Use the @JsonCollection annotation on a foreign key to invert the serialization, from one-to-one to one-to-many. You may provide the collection property name in json using argument key=<string>. For instance: @JsonCollection(key="entries")

You can find out more about these annotations with a complete example in the documentation.

Directly implementing interfaces and using traits in beans and DAOs

4 new annotations can be used to ask TDBM to implement interfaces or use traits on your beans / DAOs:

  • @AddInterface: add this annotation on your table comment to make your beans implement an interface
  • @AddInterfaceOnDao: same thing as above but on the DAO and not the bean
  • @AddTrait: use a trait on a Bean
  • @AddTraitOnDao: use a trait on a DAO

Now, there is a very important thing to be noticed.

Unless you are developing a reusable package, you probably don't need those annotations.

Indeed, you can directly add an implements or a use clause on the beans or DAOs themselves.

However, these annotations become very handy in case you do not have control over the beans or DAOs. This happens when you develop a third party package that will be used by others.

Let's say you are developing a user management package. Your package could provide a database patch that creates a "users" table. But until TDBM 5.1, it was not possible to do anything with the DAO and beans because your package had no control on the classes generation. In particular, your package cannot know the namespace of the beans and DAOs since it is configured by the end user.

Using TDBM 5.1, your package still does not know the fully qualified name of the beans and DAOs, but there is no need to! You can simply put an interface on the beans/DAOs and refer to this interface in your code:

CREATE TABLE `users` (
  `id` varchar(36) NOT NULL,
  `login` varchar(50),
  `password` varchar(50),
  PRIMARY KEY (`id`)
) COMMENT("@AddInterface(\"My\\Package\\UserInterface\")
           @AddInterfaceOnDao(\"My\\Package\\UserDaoInterface\")
           @AddTraitOnDao(\"My\\Package\\UserDaoTrait\")");   
interface UserInterface
{
   public function getLogin(): string;
   public function getPassword(): string;
}

interface UserDaoInterface
{
   public function checkCredentials(string $login, string $password): bool;
}

trait UserDaoTrait
{
   public function checkCredentials(string $login, string $password): bool
   {
       // ...
   }
}
To sum it up, use these annotations to create reusable packages that bundle database patches and behaviours relying on TDBM.

TDBMFluidSchemaBuilder

TDBM now sports a new dedicated "schema builder".

This package is not part of TDBM 5.1 but is a natural fit.

TDBM 5.1 relies a lot on annotations and TDBMFluidSchemaBuilder offers a natural way to create a DB schema with those annotations using a simple to use syntax.

// $schema is a DBAL Schema instance
$db = new TdbmFluidSchema($schema);

$posts = $db->table('posts')
            ->customBeanName('Article'); // Customize the name of the Bean class (puts a @Bean annotation on the posts table)
            ->implementsInterface('App\\PostInterface')  // Puts a @AddInterface annotationin the posts table
            ->implementsInterfaceOnDao('App\\PostDaoInterface'); // // Puts a @AddInterfaceOnDao annotationin the posts table
            ->uuid('v4') // This will generate "uuid" PK with a "@UUID" TDBM annotation that will help TDBM autogenerate the UUID
            ->column('content')->text()
            ->column('user_id')->references('users')
                               ->protectedGetter() // The Post.getUser() method is protected
                               ->protectedSetter() // The Post.setUser() method is protected
                               ->protectedOneToMany() // The User.getPosts() method is protected 

Internals: refactored DAOs and beans generation

Finally, TDBM internals have changed quite a bit too. In particular, we rewrote the code generator (used to write the beans and DAOs).

In TDBM 5.0, the code was mostly generated using templates. In TDBM 5.1, we use the zend-code library. This means that most of the code generated is represented as objects. You can now put "hooks" into code generation, and using those objects, you can change code generated on the fly.

It is now easy to add a new extension that will add/remove methods directly in the beans or DAOs, at build time!

This is for instance what the TDBM-GraphQL library does. It hooks into beans generation and adds annotations on the beans getters!

Data model generation

The TDBM team put a great deal of effort into improving the data-model mapping for corner cases.

TDBM 5.1 comes with these enhancements:

Fetch beans by id, even for composite primary keys.

To get a bean by ID, every TDBM user know he/she can use the $dao->getById($id) method. But until now, the getById was not available for composite primary keys. This is now fixed.

If you have primary keys with several columns, you can query them with $dao->getById($col1, $col2)

Handle "auto-pivot" tables

Since TDBM 4, we can detect automatically pivot tables (that are used to generate many-to-many relationships). However, there is one special case of pivot tables that was not correctly handled: pivot tables that linked to the same twice.

CREATE TABLE `people` (
  `id` int NOT NULL auto_increment,
  `name` varchar(50),
  PRIMARY KEY (`id`)
);

CREATE TABLE `relationship` (
  `parent_id` varchar(36) NOT NULL,
  `child_id` varchar(36) NOT NULL,
  PRIMARY KEY (`parent_id`, `child_id`),
  KEY (`parent_id`),
  KEY (`child_id`),
  CONSTRAINT FOREIGN KEY (`parent_id`) REFERENCES `people` (`id`),
  CONSTRAINT FOREIGN KEY (`child_id`) REFERENCES `people` (`id`)
)   

Before TDBM 5.1, TDBM would fail in seeing there are really 2 sets of getters/setters to be generated.

Starting with TDBM 5.1, you will see:

class AbstractPerson {
    public function getParents() {}
    public function setParents($parents) {}
    public function getChildren() {}
    public function setChildren($children) {}
    // ...
}

Improved "one-to-one" detection

If a column with a unique index is pointing to a foreign column (through a foreign key), you are modeling a 1..1 relationship.

But so far, TDBM did not detect the unique index and was modeling a classical 1 to many relationship.

This is now fixed and starting with TDBM 5.1!

Built on the shoulders of giants

Also, now is a good time to thank all the maintainers of the packages we are using in TDBM, and in particular the maintainers of the Doctrine DBAL library (the low level database abstraction library TDBM is built upon).

By the way, as of this writing, DBAL does not yet support adding comments at the table level on all databases. Since TDBM 5.1 relies on annotations (that are added in DB comments), we took the opportunity to contribute back to DBAL with a patch to add support for comments in all databases. The patch has been merged and will be available in DBAL 2.10!.

And as any other open-source project, we definitely welcome contributions!

Want to stay tuned on the latest TDBM releases or PHP related news? Follow me on Twitter!

About the author

David is CTO and co-founder of TheCodingMachine and WorkAdventure. He is the co-editor of PSR-11, the standard that provides interoperability between dependency injection containers. He is also the lead developper of GraphQLite, a framework-agnostic PHP library to implement a GraphQL API easily.