Experimental ActiveRecord layer on top of Doctrine2 using the Twig templating engine

Overview

Doctrine2 ActiveRecord with Twig

This is an experiment for building ActiveRecord functionality on top of Doctrine2 using the Twig templating engine. Whether it is called Propel2 or not is irrelevant.

Installation

Clone the Propel2 repository:

$ git clone --recursive https://github.com/fzaninotto/Propel2.git

Or if you use an old version of Git (pre 1.6.5):

$ git clone https://github.com/fzaninotto/Propel2.git
$ cd Propel2
$ git submodule update --init --recursive

You're good to go.

Demo

From the root of the project, type:

$ php tests/demo/demo.php

This outputs the generated code for a Base ActiveRecord class, based on a Doctrine\ORM\Mapping\ClassMetadataInfo instance.

Features

  • Working ActiveRecord builder (no handling of relations for now)
    • Generated entities are empty classes extending generated Base Entities extending nothing
    • Getter & setter generation
    • Metadata loader
    • Access to the EntityManager from within an Entity
    • fromArray and toArray
    • isNew, isModified, etc.
    • ActiveEntity methods: save(), delete()
  • Basic behavior support
    • Ability to alter the data structure
    • Ability to modify the generated code pretty much everywhere
    • Timestampable behavior example
  • Template customization via partials

Usage

No command line utility for now. You need to build classes by hand using the builders.

Example build:



// register autoloaders, etc.

use Propel\Builder\ORM\BaseActiveRecord;
use Doctrine\ORM\Mapping\ClassMetadataInfo;

$metadata = new ClassMetadataInfo('Bookstore\\Book');
$metadata->mapField(array('fieldName' => 'id', 'type' => 'integer', 'id' => true));
$metadata->mapField(array('fieldName' => 'name', 'type' => 'string'));
$metadata->mapField(array('fieldName' => 'status', 'type' => 'string', 'default' => 'published'));
// more metadata description...

$builder = new BaseActiveRecord($metadata);
echo $builder->getCode();

This generates the following code:

getId(); } if ($name === 'name') { return $this->getName(); } if ($name === 'status') { return $this->getStatus(); } throw new \InvalidArgumentException(sprintf('The property "%s" does not exists.', $name)); } /** * Load the metadata for a Doctrine\ORM\Mapping\Driver\StaticPHPDriver * * @param ClassMetadata $metadata The metadata class */ static public function loadMetadata(ClassMetadata $metadata) { $metadata->setIdGeneratorType(ClassMetadata::GENERATOR_TYPE_AUTO); $metadata->setChangeTrackingPolicy(ClassMetadata::CHANGETRACKING_DEFERRED_EXPLICIT); $metadata->setCustomRepositoryClass('Bookstore\BookRepository'); $metadata->setPrimaryTable(array('name' => 'book')); $metadata->mapField(array( 'fieldName' => 'id', 'type' => 'integer', 'id' => true, 'columnName' => 'id', )); $metadata->mapField(array( 'fieldName' => 'name', 'type' => 'string', 'columnName' => 'name', )); $metadata->mapField(array( 'fieldName' => 'status', 'type' => 'string', 'default' => 'published', 'columnName' => 'status', )); $metadata->mapOneToOne(array( 'fieldName' => 'author', 'targetEntity' => 'Doctrine\Tests\ORM\Tools\EntityGeneratorAuthor', 'mappedBy' => 'book', 'cascade' => array(), 'fetch' => ClassMetadata::FETCH_LAZY, 'joinColumns' => array(array( 'name' => 'author_id', 'referencedColumnName' => 'id', 'unique' => true, )), )); $metadata->mapManyToMany(array( 'fieldName' => 'comments', 'targetEntity' => 'Doctrine\Tests\ORM\Tools\EntityGeneratorComment', 'cascade' => array(), 'fetch' => ClassMetadata::FETCH_LAZY, 'joinTable' => array( 'name' => 'book_comment', 'joinColumns' => array(array( 'name' => 'book_id', 'referencedColumnName' => 'id', )), 'inverseJoinColumns' => array(array( 'name' => 'comment_id', 'referencedColumnName' => 'id', )), ), )); $metadata->setLifecycleCallbacks(array( 'postLoad' => array('loading'), 'preRemove' => array('willBeRemoved'), )); } /** * Populates the entity from an associative array * * This is particularly useful when populating an object from one of the * request arrays (e.g. $_POST). This method goes through the column * names, checking to see whether a matching key exists in populated * array. If so the set[ColumnName]() method is called for that column. * * @param array $array */ public function fromArray($array) { if (isset($array['id']) || array_key_exists('id', $array)) { $this->setId($array['id']); } if (isset($array['name']) || array_key_exists('name', $array)) { $this->setName($array['name']); } if (isset($array['status']) || array_key_exists('status', $array)) { $this->setStatus($array['status']); } } /** * Exports the entity to an associative array * * @return array */ public function toArray() { return array( 'id' => $this->getId(), 'name' => $this->getName(), 'status' => $this->getStatus(), ); } } ">


namespace Bookstore\Base;

use Doctrine\ORM\Mapping\ClassMetadata;
use Propel\ActiveEntity;

/**
 * Base class providing ActiveRecord features to Book.
 * Do not modify this class: it will be overwritten each time you regenerate
 * ActiveRecord.
 */
class Book extends ActiveEntity
{
    /**
     * @var integer
     */
    protected $id;

    /**
     * @var string
     */
    protected $name;

    /**
     * @var string
     */
    protected $status = 'published';

    /**
     * @var \Doctrine\Tests\ORM\Tools\EntityGeneratorAuthor
     */
    protected $author;

    /**
     * @var \Doctrine\Common\Collections\ArrayCollection
     */
    protected $comments;

    /**
     * Class constructor.
     * Initializes -to-many associations
     */
    public function __construct()
    {
        $this->comments = new \Doctrine\Common\Collections\ArrayCollection();
    }

    /**
     * Get the id
     * 
     * @return integer
     */
    public function getId()
    {
        return $this->id;
    }

    /**
     * Set the id
     * 
     * @param integer $id
     */
    public function setId($id)
    {
        $this->id = $id;
    }

    /**
     * Get the name
     * 
     * @return string
     */
    public function getName()
    {
        return $this->name;
    }

    /**
     * Set the name
     * 
     * @param string $name
     */
    public function setName($name)
    {
        $this->name = $name;
    }

    /**
     * Get the status
     * 
     * @return string
     */
    public function getStatus()
    {
        return $this->status;
    }

    /**
     * Set the status
     * 
     * @param string $status
     */
    public function setStatus($status)
    {
        $this->status = $status;
    }

    /**
     * Get the related author
     * 
     * @return \Doctrine\Tests\ORM\Tools\EntityGeneratorAuthor
     */
    public function getAuthor()
    {
        return $this->author;
    }

    /**
     * Set the related author
     * 
     * @param \Doctrine\Tests\ORM\Tools\EntityGeneratorAuthor $author
     */
    public function setAuthor($author)
    {
        $this->author = $author;
    }

    /**
     * Get the collection of related comments
     * 
     * @return \Doctrine\Common\Collections\ArrayCollection
     */
    public function getComments()
    {
        return $this->comments;
    }

    /**
     * Set the collection of related comments
     * 
     * @param \Doctrine\Common\Collections\ArrayCollection $comments
     */
    public function setComments($comments)
    {
        $this->comments = $comments;
    }

    /**
     * Add a comment to the collection of related comments
     * 
     * @param Doctrine\Tests\ORM\Tools\EntityGeneratorComment $comment
     */
    public function addComment($comment)
    {
        $this->comments->add($comment);
    }

    /**
     * Remove a comment from the collection of related comments
     * 
     * @param Doctrine\Tests\ORM\Tools\EntityGeneratorComment $comment
     */
    public function removeComment($comment)
    {
        $this->comments->removeElement($comment);
    }

    /**
     * Set a property of the entity by name passed in as a string
     * 
     * @param string $name  The property name
     * @param mixed  $value The value
     * 
     * @throws \InvalidArgumentException If the property does not exists
     */
    public function setByName($name, $value)
    {
        if ($name === 'id') {
            return $this->setId($value);
        }
        if ($name === 'name') {
            return $this->setName($value);
        }
        if ($name === 'status') {
            return $this->setStatus($value);
        }

        throw new \InvalidArgumentException(sprintf('The property "%s" does not exists.', $name));
    }

    /**
     * Retrieve a property from the entity by name passed in as a string
     * 
     * @param string $name  The property name
     * 
     * @return mixed The value
     * 
     * @throws \InvalidArgumentException If the property does not exists
     */
    public function getByName($name)
    {
        if ($name === 'id') {
            return $this->getId();
        }
        if ($name === 'name') {
            return $this->getName();
        }
        if ($name === 'status') {
            return $this->getStatus();
        }

        throw new \InvalidArgumentException(sprintf('The property "%s" does not exists.', $name));
    }

    /**
     * Load the metadata for a Doctrine\ORM\Mapping\Driver\StaticPHPDriver
     *
     * @param ClassMetadata $metadata The metadata class
     */
    static public function loadMetadata(ClassMetadata $metadata)
    {
        $metadata->setIdGeneratorType(ClassMetadata::GENERATOR_TYPE_AUTO);
        $metadata->setChangeTrackingPolicy(ClassMetadata::CHANGETRACKING_DEFERRED_EXPLICIT);
        $metadata->setCustomRepositoryClass('Bookstore\BookRepository');
        $metadata->setPrimaryTable(array('name' => 'book'));

        $metadata->mapField(array(
            'fieldName'  => 'id',
            'type'       => 'integer',
            'id'         => true,
            'columnName' => 'id',
        ));
        $metadata->mapField(array(
            'fieldName'  => 'name',
            'type'       => 'string',
            'columnName' => 'name',
        ));
        $metadata->mapField(array(
            'fieldName'  => 'status',
            'type'       => 'string',
            'default'    => 'published',
            'columnName' => 'status',
        ));

        $metadata->mapOneToOne(array(
            'fieldName'         => 'author',
            'targetEntity'      => 'Doctrine\Tests\ORM\Tools\EntityGeneratorAuthor',
            'mappedBy'          => 'book',
            'cascade'           => array(),
            'fetch'             => ClassMetadata::FETCH_LAZY,
            'joinColumns'       => array(array(
                'name'                 => 'author_id',
                'referencedColumnName' => 'id',
                'unique'               => true,
            )),
        ));
        $metadata->mapManyToMany(array(
            'fieldName'         => 'comments',
            'targetEntity'      => 'Doctrine\Tests\ORM\Tools\EntityGeneratorComment',
            'cascade'           => array(),
            'fetch'             => ClassMetadata::FETCH_LAZY,
            'joinTable'         => array(
                'name'               => 'book_comment',
                'joinColumns'        => array(array(
                    'name'                 => 'book_id',
                    'referencedColumnName' => 'id',
                )),
                'inverseJoinColumns' => array(array(
                    'name'                 => 'comment_id',
                    'referencedColumnName' => 'id',
                )),
            ),
        ));

        $metadata->setLifecycleCallbacks(array(
            'postLoad'  => array('loading'),
            'preRemove' => array('willBeRemoved'),
        ));
    }

    /**
     * Populates the entity from an associative array
     * 
     * This is particularly useful when populating an object from one of the
     * request arrays (e.g. $_POST). This method goes through the column
     * names, checking to see whether a matching key exists in populated
     * array. If so the set[ColumnName]() method is called for that column.
     * 
     * @param array $array
     */
    public function fromArray($array)
    {
        if (isset($array['id']) || array_key_exists('id', $array)) {
            $this->setId($array['id']);
        }
        if (isset($array['name']) || array_key_exists('name', $array)) {
            $this->setName($array['name']);
        }
        if (isset($array['status']) || array_key_exists('status', $array)) {
            $this->setStatus($array['status']);
        }
    }

    /**
     * Exports the entity to an associative array
     * 
     * @return array
     */
    public function toArray()
    {
        return array(
            'id' => $this->getId(),
            'name' => $this->getName(),
            'status' => $this->getStatus(),
        );
    }

}

Extending ActiveEntity is optional, but gives the same API as Propel1 (save(), delete(), isNew(), etc.).

Comments
  • fixed empty lines from rendering, and few improvement in docblock and fix

    fixed empty lines from rendering, and few improvement in docblock and fix

    Hello François, here is another PR ;)

    • fixed empty lines from rendering (no more double empty lines)
    • few improvement in docblock (real type instread of mixed, and removed unnecessary dot)
    • fixed fromArray method which prevent passing 'null' values
    opened by brikou 5
  • [Template] switch statement + fromArray calls setByName

    [Template] switch statement + fromArray calls setByName

    • added missing slash + typo
    • made use of switch statement in generic setter/getter
    • method fromArray calls setByName which handles perfectly exception when generic setter is not defined
    opened by brikou 4
  • Same PR as the previous one but for the

    Same PR as the previous one but for the "traits" branch

    • strict_variables allows Twig to warn you when you use a non-existent variable/method/...
    • debug allows the templates to be automatically reloaded
    • cache allows to have better error messages
    opened by fabpot 3
  • Fixed Twig config and templates + moved to git submodule

    Fixed Twig config and templates + moved to git submodule

    The first commit fixes Twig integration. The second one moves dependencies to submodules as it makes things much easier for people wanting to test Propel2.

    opened by fabpot 2
  • updated exportArray to make it smarter (check TwigBuilderTest to see exam

    updated exportArray to make it smarter (check TwigBuilderTest to see exam

    • updated exportArray to make it smarter (check TwigBuilderTest to see examples)

    you can check my PR about readme to check what has changed https://github.com/fzaninotto/Propel2/pull/17/files

    opened by brikou 1
  • exportArray now displays aligned values

    exportArray now displays aligned values

    before:

    // ...
    'cascade' => array(),
    'fetch' => ClassMetadata::FETCH_LAZY,
    'joinTable' => array(
        'name' => 'book_comment',
        'joinColumns' => array(
            0 => array(
                'name' => 'book_id',
                'referencedColumnName' => 'id',
            ),
        ),
        'inverseJoinColumns' => array(
            0 => array(
                'name' => 'comment_id',
                'referencedColumnName' => 'id',
            ),
        ),
    

    after:

    // ...
    'cascade'   => array(),
    'fetch'     => ClassMetadata::FETCH_LAZY,
    'joinTable' => array(
        'name'        => 'book_comment',
        'joinColumns' => array(
            0 => array(
                'name'                 => 'book_id',
                'referencedColumnName' => 'id',
            ),
        ),
        'inverseJoinColumns' => array(
            0 => array(
                'name'                 => 'comment_id',
                'referencedColumnName' => 'id',
            ),
        ),
    
    opened by brikou 0
  • Tweaked default configuration for Twig

    Tweaked default configuration for Twig

    Changing these defaults allows for a better experience when problems occur.

    • strict_variables allows Twig to warn you when you use a non-existent variable/method/...
    • debug allows the templates to be automatically reloaded
    • cache allows to have better error messages
    opened by fabpot 0
  • some work in progress suggestions

    some work in progress suggestions

    added persist() method to BaseActiveRecord, allowing to save() without flushing the EntityManager (for batch persisting). added pre_() and post_() hooks in propel1 fashion

    I looked into the Doctrine2 docs and found that the existing Doctrine2 events for prePersist etc. lack some functionality, e.g. directly differentiating between insert and update. They also have to be declared as lifecycle-callbacks in the schema if to be used directly.

    My long-term suggestion is modifying the schema to always include the lifecycle-callbacks automatically and explicitly creating the hook methods based on the events, to stay compatible to D2.

    opened by vworldat 0
Owner
Francois Zaninotto
Open-source software enthusiast, React.js, Node.js and PHP expert, Lean & Agile management practicioner, Leadership & motivation experimenter, CEO @marmelab
Francois Zaninotto
SwitchBlade: Custom Directives for the Laravel Blade templating engine

SwitchBlade: Custom Directives for the Laravel Blade templating engine

Awkward Ideas 10 Nov 29, 2022
Twig Template Engine to Phalcon PHP

Twig Template Engine to Phalcon PHP

Vinicius 4 Oct 7, 2022
Provides TemplateView and TwoStepView using PHP as the templating language, with support for partials, sections, and helpers.

Aura View This package provides an implementation of the TemplateView and TwoStepView patterns using PHP itself as the templating language. It support

Aura for PHP 83 Jan 3, 2023
A faster, safer templating library for PHP

Brainy Brainy is a replacement for the popular Smarty templating language. It is a fork from the Smarty 3 trunk. Brainy is still very new and it's lik

Box 66 Jan 3, 2023
The Templating component provides all the tools needed to build any kind of template system.

Templating Component The Templating component provides all the tools needed to build any kind of template system. It provides an infrastructure to loa

Symfony 999 Dec 25, 2022
Simple PHP templating system for user editable templates.

Simple template Simple PHP templating system for user editable templates. Idea Most applications need to render templates that insert safely treated v

Baraja packages 1 Jan 23, 2022
Twig, the flexible, fast, and secure template language for PHP

Twig, the flexible, fast, and secure template language for PHP Twig is a template language for PHP, released under the new BSD license (code and docum

Twig 7.7k Jan 1, 2023
Multi target HAML (HAML for PHP, Twig, )

Multi target HAML MtHaml is a PHP implementation of the HAML language which can target multiple languages. Currently supported targets are PHP and Twi

Arnaud Le Blanc 363 Nov 21, 2022
Smarty is a template engine for PHP, facilitating the separation of presentation (HTML/CSS) from application logic.

Smarty 3 template engine smarty.net Documentation For documentation see www.smarty.net/docs/en/ Requirements Smarty can be run with PHP 5.2 to PHP 7.4

Smarty PHP Template Engine 2.1k Jan 1, 2023
☕ Latte: the intuitive and fast template engine for those who want the most secure PHP sites.

Latte: amazing template engine for PHP Introduction Latte is a template engine for PHP which eases your work and ensures the output is protected again

Nette Foundation 898 Dec 25, 2022
PHP Template Attribute Language — template engine for XSS-proof well-formed XHTML and HTML5 pages

PHPTAL - Template Attribute Language for PHP Requirements If you want to use the builtin internationalisation system (I18N), the php-gettext extension

PHPTAL 175 Dec 13, 2022
View template engine of PHP extracted from Laravel

Blade 【简体中文】 This is a view templating engine which is extracted from Laravel. It's independent without relying on Laravel's Container or any others.

刘小乐 143 Dec 13, 2022
PHP template engine for native PHP templates

FOIL PHP template engine, for PHP templates. Foil brings all the flexibility and power of modern template engines to native PHP templates. Write simpl

Foil PHP 167 Dec 3, 2022
⚡️ Simple and fastly template engine for PHP

EasyTpl ⚡️ Simple and fastly template engine for PHP Features It's simple, lightweight and fastly. No learning costs, syntax like PHP template It is s

PHPPkg 19 Dec 9, 2022
Liquid template engine for PHP

Liquid is a PHP port of the Liquid template engine for Ruby, which was written by Tobias Lutke. Although there are many other templating engines for PHP, including Smarty (from which Liquid was partially inspired)

Harald Hanek 230 Aug 18, 2022
Pug (Jade) template engine for Symfony

Pug-Symfony Pug template engine for Symfony This is the documentation for the ongoing version 3.0. Click here to load the documentation for 2.8 Instal

Pug PHP 41 Dec 16, 2022
PHPlater, a simple template engine.

PHPlater A simple PHP template engine that lets PHP do all the logic and then append it to the HTML in the template file. It is set to solve the probl

John Larsen 2 Jun 3, 2022
Provides a GitHub repository template for a PHP package, using GitHub actions.

php-package-template Installation ?? This is a great place for showing how to install the package, see below: Run $ composer require ergebnis/php-pack

null 280 Dec 27, 2022
UserFrosting is a secure, modern user management system written in PHP and built on top of the Slim Microframework, Twig templating engine, and Eloquent ORM.

UserFrosting is a secure, modern user management system written in PHP and built on top of the Slim Microframework, Twig templating engine, and Eloquent ORM.

UserFrosting 1.6k Jan 1, 2023
UserFrosting is a secure, modern user management system written in PHP and built on top of the Slim Microframework, Twig templating engine, and Eloquent ORM.

UserFrosting 4.6 Branch Version Build Coverage Style master hotfix develop https://www.userfrosting.com If you simply want to show that you like this

UserFrosting 1.6k Jan 1, 2023