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.