Getting Started with Doctrine in Concrete CMS
Getting Started with Doctrine in Concrete CMS
Doctrine is a very flexible, simple (once you get to know it better) and powerful PHP library for database interactions primarily focused on the ORM = Object Relational Mapping and DBAL = DataBase Abstraction Layer.
The project website where you can get all required information is https://www.doctrine-project.org
The main words you will see most often in Doctrine:
Persistence - storing or saving data in storage
Entity - PHP object that can be identified over many requests by a unique identifier or primary key
What is ORM
From Wikipedia:
ORM is a programming technique for converting data between incompatible type systems using object-oriented programming languages. It translates the logical representation of the objects into a form that is capable of being stored in the database while preserving the properties of the objects and their relationships so that they can be reloaded as objects when needed.
From Doctrine Project:
Doctrine ORM provides transparent persistence for PHP objects. It uses the Data Mapper pattern at the heart, aiming for a complete separation of your domain/business logic from the persistence in a relational database management system. The benefit of Doctrine for the programmer is the ability to focus on the object-oriented business logic and worry about persistence only as a secondary problem.
An entity contains persistable properties. A persistable property is an instance variable of the entity that is saved into and retrieved from the database by Doctrine's data mapping capabilities.
Enough with pedantic jargon, you can read all the nitty-gritty detail on the project website and internet. Let's see how we can use Doctrine in Concrete CMS. In this guide we'll learn how to manage a database in a package with Doctrine, in particular:
- How to create a database table with mapping PHP objects
- How to to insert, update, delete and find objects in the database
A prerequisite to this tutorial is knowledge of OOP concepts and using custom code in Concrete CMS packages (https://documentation.concrete5.org/developers/packages/overview).
The tutorial can be adapted for non-package use by Concrete CMS overrides.
As we are talking about mapping, the most important things to remember are that classes are mapped to database tables, class properties are mapped to table columns, class methods are mapped to database queries. Tables, columns and relationships are defined using annotations.
To start with, let's consider a simple skeleton package which has 1 database table where some variables are stored - this variables are said to be persisted. The table will be mapped from a Skeleton PHP class. It doesn't matter how the class is called. However it's essential that the class name and namespace are consistent and in accordance with Concrete CMS guidelines. That is:
The package custom source is in the packages/ab_package_skeleton/src/PackageSkeleton folder.
The package controller defines the $pkgAutoloaderRegistries and implements the getEntityManagerProvider():
class Controller extends Package implements ProviderAggregateInterface { protected $pkgAutoloaderRegistries = [ 'src/PackageSkeleton' => 'PackageSkeleton' ]; public function getEntityManagerProvider() { $provider = new StandardPackageProvider($this->app, $this, [ 'src/PackageSkeleton' => 'PackageSkeleton' ]); return $provider; }
The Skeleton class is defined in the same file name Skeleton.php in the packages/ab_package_skeleton/src/PackageSkeleton/Skeleton folder
The Skeleton class has the following namespace and requires the following Doctrine classes:
namespace PackageSkeleton\Skeleton; use Doctrine\ORM\Mapping as ORM; use Concrete\Core\Support\Facade\DatabaseORM;
Now to creating the table. You can call it whatever, but I'd recommend to call it with a reference to the class name, i.e. AbPackageSkeletonSkeletons. When you have multiple tables and multiple packages, to avoid clashes (what if someone else in another package has a table with the same name?). To create the table the following annotation syntax is used:
/** * @ORM\Entity * @ORM\Table(name="AbPackageSkeletonSkeletons") */
Then we need to define some table columns, e.g. a column 'id' - this will be a unique autoincementing integer for each entry or row and it will be mapped from the class property $id, and a column 'name' - this will store strings for each entry with the max length of 255 characters which cannot be empty and it will be mapped from the class property $name. To create the table columns the following annotation syntax is used:
/** * @ORM\Id * @ORM\Column(type="integer") * @ORM\GeneratedValue(strategy="AUTO") */ protected $id; /** * @ORM\Column(type="string",length=255,nullable=false) */ protected $name;
Note the property names are mapped to the exactly the same column names.
So to get a table cell name value all you need is to know the value of the $name property. How to get that? As simple as as this:
public function getName() { return $this->name; }
Saving a name is also as simple as this:
public function setName($name) { $this->name = $name; }
The above getter and setter methods allow you to work with the table cell once the entity class is instantiated. So to get and set the table names from outside of the class, you simply use the class instance like so:
use PackageSkeleton\Skeleton\Skeleton; $skeleton = $app->make(Skeleton::class); $name = $skeleton->getName(); $skeleton->setName('some string');
An important thing to note is setting the class property value does just and only that, but it does NOT save or rather persist the value to the database. To persist it you need to ... well, persist it and then flush it to execute all updates:
public function save() { $em = DatabaseORM::entityManager(); $em->persist($this); $em->flush(); }
Deleting an entry is similar:
public function delete() { $em = DatabaseORM::entityManager(); $em->remove($this); $em->flush(); }
For getting and setting the name values we need to know the entry IDs. We can use the Doctrine entityManager() to get the particular entry by its ID:
public static function getByID($id) { $em = DatabaseORM::entityManager(); return $em->find(get_class(), $id); }
So to do that from outside the class we can do the following, e.g. get an entry with ID = 5 and save a different value of the name:
use PackageSkeleton\Skeleton\Skeleton; $skeleton = $app->make(Skeleton::class); $n = $skeleton->getByID(5); echo $n; $n = 'other name'; $skeleton->setName($n); $skeleton->save();
Note: If you later have added new table columns, clear the cache. Otherwise the ORM might ignore the new table columns when updating the entity.
TODO:
- Associations
- Searching
- Sorting
- Filtering
Further useful resources:
https://github.com/linuxoid/ab_project_skeleton - source code of the skeleton package
https://www.doctrine-project.org/projects/doctrine-orm/en/2.6/reference/working-with-objects.html