Getting Started with Doctrine in Concrete CMS

This is a community-contributed tutorial. This tutorial is over a year old and may not apply to your version of Concrete CMS.
Jan 20, 2021

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

http://www.inanzzz.com/index.php/posts/doctrine

https://symfony.com/doc/current/doctrine.html

Recent Tutorials
Redirect all requests to HTTPS
Oct 9, 2024
By myq.

How to follow best practices for a secure web

Upgrade Concrete versions 9.3.1 and 9.3.2
Sep 10, 2024
By myq.

How to get past a bug in versions 9.3.1 and 9.3.2 that prevents upgrading the Concrete core through the Dashboard

How to use Composer with Marketplace extensions
Aug 22, 2024

Composer can be used to manage third-party extensions from the marketplace

Controlling Google Tag Manager Tags Based on Concrete CMS Edit Toolbar Visibility
Aug 13, 2024

This document provides a step-by-step guide on how to control the firing of Google Tag Manager (GTM) tags based on the visibility of the Concrete CMS edit toolbar. It explains how to create a custom JavaScript variable in GTM to detect whether the edit toolbar is present on a page and how to set up a trigger that ensures GTM tags only fire when the toolbar is not visible. This setup is particularly useful for developers and marketers who want to ensure that tracking and analytics tags are not activated during content editing sessions, thereby preserving the accuracy of data collected.

Upgrading Concrete from 8.5 to 9.x
Jun 21, 2024
By myq.

How to avoid problems upgrading from 8.5 to 9.x

How to change the default date format
May 30, 2024
By myq.

Change the format of the default date

Improvements?

Let us know by posting here.