Skip to content

restouffer/SimpleThingsTransactionalBundle

 
 

Repository files navigation

SimpleThings TransactionalBundle

Wraps calls to controllers into a transaction, be it Doctrine DBAL or Persistence Managers (ORM, MongoDB, CouchDB). Configuration is done via routing parameters or through a list of controllers/actions configured in the extension config.

Installation

See at the end of this document.

Problem

Symfony2 allows to nest controllers into each other in unlimited amounts. These controllers can all modify and save data, probably with different transactional needs. The Doctrine persistence solutions (ORM, MongoDB, CouchDB) use a transactional write-behind mechanism to flush changes in batches, best executed at the end of the master request. If each controller or model service handles transactions themselves then you probably overuse the flush operation, which can lead to inconsistencies and performance penalities.

These flushes should not be executed in the model/services but should be handled by the controller layer, because it knows when all operations are done.

How it works

For every Doctrine DBAL connection, every EntityManager and every DocumentManager the Transactional Bundle creates a service that implements a transactions manager interface:

interface TransactionManagerInterface
{
    function beginTransaction();
    function commit();
    function rollBack();
}

With the transactional bundle the following workflow is applied to an action that is marked as transactional (by default always if POST, PUT, DELETE, PATCH request is found).

  1. Detect which Transaction Manager(s) should wrap the to-be-excecuted action.
  2. A transaction is started before the controller is called.
  3. The controller execution is wrapped in a try-catch block
  4. On successful response generation (status code < 400) the transaction is committed. This includes a call to EntityManager::flush or DocumentManager::flush in case of an orm, mongodb or couchdb "transaction".
  5. On status-code >= 400 the transaction is rolled back.
  6. If an exception is thrown the transaction is rolled back.

Each transaction manager is named like the manager it belongs to:

doctrine.orm.default_entity_manager => simplethings_tx.orm.default
doctrine_mongodb.odm.default_document_manager => simplethings_tx.mongodb.default
doctrine_couchdb.odm.default_document_manager => simplethings_tx.couchdb.default

You can mark actions as transactional by means of configuration. There are three different ways to configure the transactional behavior:

Working with a default transaction manager

If you have a small RESTful application and you only use one transactional manager, for example the Doctrine ORM then your configuration is as simple as configuring the transactional managers name in the app/config/config.yml extension configuration:

simple_things_transactional:
    auto_transactional: true
    defaults:
        conn: ["orm.default"]

With this configuration every POST, PUT, DELETE and PATCH request is wrapped inside a transaction of the given connection. There is no way to disable this behavior except by throwing an exception. GET requests that need to write a transaction have to do this explicitly.

Working with explicit configuration

If you have an application that is either not RESTful, uses multiple transactional managers or has advanced requirements with regard to transactions then you should configure the transactional behavior explicitly.

You can do so by matching fcqn controller and action names with regular expression. Every transactional configuration that matches for a given controller+action combination is started. If a transaction is started for a connection multiple times then an exception is thrown.

simple_things_transactional:
    defaults:
        conn: ["mongodb.default"]
        methods: ["POST", "PUT", "DELETE", "PATCH"]
    patterns:
        fos_user:
            pattern: "FOS\(.*)Controller::(.*)Action"
            # not giving conn: uses the default
            propagation: REQUIRES_NEW
            noRollbackFor: ["NotFoundHttpException"]
            subrequest: true
        acme:
            pattern: "Acme(.*)"
            conn: ["orm.default", "couchdb.default"]
            subrequest: false
        acme_logging:
            pattern: "Acme\DemoBundle\Controller\IndexController::logAction"
            conn: ["dbal.other"]
            methods: ["GET"]

Annotations

You can also configure transactional behavior with annotations. The configuration for annotations is as simple as:

simple_things_transactional:
    annotations: true

The previous Acme\DemoBundle\Controller\IndexController then looks like:

namespace Acme\DemoBundle\Controller;

use SimpleThings\TransactionalBundle\Annotations AS Tx;

/**
 * @Tx\Transactional(conn={"orm.default", "couchdb.default"})
 */
class IndexController
{
    /**
     * @Tx\Transactional(conn={"orm.other"}, methods: {"GET"})
     */
    public function demoAction()
    {

    }
}

Example

Using the previous routes as example here is a sample action that does not require any calls to EntityManager::flush anymore.

class PostController extends Controller
{
    public function editAction($id)
    {
        $em = $this->container->get('doctrine.orm.default_entity_manager');
        $post = $em->find('Post', $id);

        if ($this->container->get('request')->getMethod() == 'POST') {
            $post->modifyState();
            // no need to call $em->flush(), the flush is executed in a transactional wrapper

            return $this->redirect($this->generateUrl("view_post", array("id" => $post->getId()));
        }

        return $this->render("MyBlogBundle:Post:edit.html.twig", array());
    }
}

EntityManager#flush() is only called when the requet is using the POST-method.

Installation

  1. Add TransactionalBundle to deps:

     [SimpleThingsTransactionalBundle]
     git=git@github.com:simplethings/SimpleThingsTransactionalBundle.git
     target=/bundles/SimpleThings/TransactionalBundle
    
  2. Run ./bin/vendors to upgrade vendors and include TransactionalBundle

  3. Add Bundle to app/AppKernel.php

     public function registerBundles()
     {
         $bundles = array(
             //..
             new SimpleThings\TransactionalBundle\SimpleThingsTransactionalBundle(),
             //..
         );
     }
    
  4. Add to autoload.php

     'SimpleThings'     => __DIR__.'/../vendor/bundles',
    
  5. Configure extension:

     simple_things_transactional: ~
    

Todos

  • Implement Propagation
  • Implement Isolation
  • Try to evaluate if hooking into exception_handler is a killing exceptions from controllers more gracefully and not having them loose the stack trace.

About

Convenienantly wrap controllers into db or object transactions through routing definitions.

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published

Languages

  • PHP 100.0%