Worker classes in symfony – Part 2

This is the second and final part of my series about worker classes in symfony.
The first part has shown, how worker classes could make development easier, if the main logic of your project is moved there. This article is focused on the scalability fact of those classes.
Let’s switch from the newsletter example from the first article to another one, which may show up the scalability advantage better: fetching an user object by its primary key/id.

Jumping back to an normal action, it could look like this (using Propel):

1
2
3
4
5
6
7
8
class userActions extends sfActions
{
  public function executeProfile(sfWebRequest $request)
  {
    $this->user = UserPeer::retrieveByPK($request->getParameter('id'));
    $this->forward404Unless($this->user);
  }
}

This is quite simple and should look familiar to you. Note that i ignored the possibility to fetch the user object directly from the routing, otherwise it would be hard to explain the next steps.

There are two problems we are faced to, if we want to scale this action. The first one is the restriction to Propel query code (here it is: UserPeer::retrieveByPK). Switching to another ORM requires a lot of time, while browsing through the complete project and replacing ORM-specific code.
The second one is the returned model class itself. The model defines no clear interface to its accessors. Getters, setters or attributes will be added and droped if you change something in its according schema. It is even more work, when you have to adept the code provided by the model class before you can switch to another ORM.
In fact, it would be quite easier to change from Propel to Doctrine, but only imagine it the other way round…
I am also aware of the existance of the DBFinderPlugin, which provides a common interface for both Propel and Doctrine. But what will happen, if you do not use one of those ORMs? Yes, it would not work and therefore we could not use it either.

Ok, how would the code of the first listing look like using a worker class?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
class UserWorker extends sfBaseWorker
{
  /**
  * Returns an hydrated user object by user id
  *
  * @param $int id the user id
  *
  * @throws UserWorkerException
  *
  * @return User
  */
  public function getById($id)
  {
    if(!$user = UserPeer::retrieveByPK($id))
	{
		throw new UserWorkerException(sprintf('User with id %s could not be fetched', $id));
	}
 
	return $user;
  }
}
 
class userActions extends sfActions
{
  public function executeProfile(sfWebRequest $request)
  {
    $worker = new UserWorker($this->dispatcher, $this->context);
    $this->user = $worker->getById($request->getParameter('id'));
    $this->forward404Unless($this->user);
  }
}

Did we solved our two problems now? Unfortunately not yet completely. Our action is freed from ORM-specific query code, but it still processes with the returned user model class.

The solution
We solve the problem, when we introduce proxy classes and well defined interfaces for our models. Sounds complex? It might be :). Let’s see some code, here we go:

A very simple and shortend interface for the user model could be:

1
2
3
4
5
6
7
8
9
10
interface UserInterface
{
  public function getId();
 
  public function getName();
 
  public function setName($v);
 
  public function save();
}

The implementing proxy class for the user model:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
class UserProxy implements UserInterface
{
 
  private $user = null;
 
  public function __construct(User $user)
  {
    $this->user = $user;    
  }
 
  public function getId()
  {
    return $this->user->getId();
  }
 
  public function getName()
  {
    return $this->user->getName();
  }
 
  public function setName($v)
  {
    return $this->user->setName($v);
  }
 
  public function save()
  {
    return $this->user->save();
  }
}

Adding this proxy class to our user worker:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class UserWorker extends sfBaseWorker
{
  /**
  * Returns an hydrated proxy user object by user id
  *
  * @param $int id the user id
  *
  * @throws UserWorkerException
  *
  * @return UserProxy
  */
  public function getById($id)
  {
    if(!$user = UserPeer::retrieveByPK($id))
    {
	throw new UserWorkerException(sprintf('User with id %s could not be fetched', $id));
    }
 
    return new UserProxy($user);
  }
}

Now we are done. The action (or template) does not process anymore with a concrete ORM model. With the help of the proxy class, we can change the underlying model layer without breaking anything in the action. By providing a well defined interface, the proxy class manages how a model could be modified and accessed.

At this point, you should understand the reason for all those changes. It may look like a huge overhead for just retrieving some data out of the model. But in our case it was a precondition for our global architecture.
Our models and our main business logic is partly served by and processed in SOA applications (there we use Propel and i hope in some near days also Doctrine). I implemented a mechanism, which allows worker classes to access those SOA applications very easily. They are fetching the serialized data from those applications, hydrate them into proxy classes and provide them to the rest of the project.

I could not show the original code, but it looks similar to this (it differs in little parts from the former listings):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
class UserWorker extends sfBaseWorker
{
  /**
  * Returns an hydrated proxy user object by user id
  *
  * @param $int id the user id
  *
  * @throws UserWorkerException
  *
  * @return UserProxy
  */
  public function getById($id)
  {
    $soaQuery = new SoaQuery();
    $soaQuery->setAction('user', 'getById');
    $soaQuery->setParams(array('id' => $id));
 
    if(!$serializedData = $soaQuery->execute())
    {
	throw new UserWorkerException('some soa error ....');
    }
 
    $proxy = new UserProxy();
    $proxy->hydrate($serializedData);
    return $proxy;
  }
}

In this way, action code is really short, it is completely decoupled from the logic how the model is originally handled and it works quite well. If we decide to change something in our architecture some day, we only have to change our worker and proxy classes. The rest of the project will not notice those changes, as it should be in a layered architecture.

Sure, this step is nothing for small applications. But when your project grows and grows, the number of requests in a second turns from dozens to hundreds, then you’ll be thankful, if your project provides this approach.

Tags: ,

Author: BleedingMoon

4 Responses to “Worker classes in symfony – Part 2”

  1. Krasimir Krasimir says:

    Hi and thanks for the post.
    Do you plan to release sfBaseWorker code in some way (plugin for example) ? BTW there is already plugin named sfWorkerPLugin (http://www.symfony-project.org/plugins/sfWorkerPlugin), but it does a different job :)

  2. annis annis says:

    Hey there. This is an interesting article but there’s one thing I wanted to clarify. Yes, the DbFinderPlugin, as is now, works with only Propel and Doctrine but the documentation clearly states that, using a custom finder, (I quote from the DbFinder README) “… you can use the finder API to query model objects that are not backed by any ORM at all”. Just a hint, now I have to go and read the first article you wrote! :) But it is an interesting concept, especially when you really want to develop plugins for Symfony that can be used outside of Symfony and still work, even without Propel or Doctrine, but maybe with NHibernate. ;-) Cheers, Daniel

  3. Frank Stelzer BleedingMoon says:

    @Krasimir
    As i mentioned it already in the comments of the first article, the body of the sfBaseWorker is almost empty. It just recieves the dispatcher and the context in the constructor. I am not quite sure, what the base could should be, but i thought it’s better providing a parent class for all workers.

  4. Toni Uebernickel havvg says:

    Dunno whether it was intended to do this kind of work, but I like the thoughts about these worker classes and used the concept to put some business logic together, that tangents more than one model and is used in more than one module.

    Putting all pieces of the models together in one central “library” that is independent from the controllers itself is just great :)

    Thanks for these posts!

Leave a Reply