There's a new player in town, and he brought new toys: The PHP World welcomes FLOW3, an enterprise application framework written and backed by the community of the TYPO3 CMS. FLOW3 can be used as standalone full-stack framework for your applications. It's interesting, because it introduces some concepts of software development that haven't been adapted to PHP before.
Among these new concepts is "Aspect Oriented Programming". We will have a look on the theory of the pattern, and will set up a basic FLOW3 Application and weave in our own aspect!
Why Should I Care?
If a new framework hits stable, an excellent question to rise might be: What can it do that the tools that I love can't?
PHP already has an armada of excellent frameworks and most of them claim to be written with separation of concerns in mind. So does FLOW3.
In terms of software development this means, that the classes that are implementing the logic of your application should only care about one thing. For example, a mailer class should send mails. It should not retrieve potential receivers from a database.
All modern frameworks (including FLOW3) push a lot of patterns into the software stack that do a great job at separating the concerns of your business logic; among them the famous MVC that is separating your logic into different layers.
However, an application is not only built on business logic alone. As it grows, you may want to implement additional services, features, plugins or plugins of plugins. You surely don't want this stuff in your business logic! But what are your options?
Let's say you want to implement a logging service that writes some stuff to a text file every time a specific set is deleted from the database. Naturally, the logging service is not part of your database layer. But in order to make it work, you have to place some code right there, like this:
/** * Delete a user * @param UserModel $user * @return void */ public function deleteUser(UserModel $user) { $this->loggingService->log('removing user ' . $user->getName()); $this->users->remove($user); }
Horrible, isn't it? Let's face it: The logging service adds dirt to your code. You have to touch the original code in order to implement it and you add an additional dependency, in this case an instance of the LoggingService.
Hence, you're adding complexity. And you likely will have to add it all over your app (depending on what else you want to log).
You may come up with an pattern like an event dispatcher or a hook system like it's implemented in Drupal and Wordpress and then have your logger listen for events:
/** * Delete a user * @param UserModel $user * @return void */ public function deleteUser(UserModel $user) { $this->eventDispatcher->dispatch(self::USER_DELETED); $this->users->remove($user); }
This is better, but it's still not cool. You're code is still dependency aware: it has some code with the sole purpose of informing other parties that something happens.
This has some bad side-effects: What if you want to log something, but there's not an event dispatched at that time? What if you haven't written that code because you are writing a plugin? You'd have to change the original code – not once, but every time an update is available.
Let's call the logging service an aspect and see how FLOW3 takes care of the problem.
Aspect Weaving
Our logging service is only interested in one thing regarding the existing code: It wants to intercept it in some classes at various points (e .g. when something is deleted). With the implementation discussed above, the existing code controls the points of execution.
Wouldn't it be nice if the service could define a set of rules at which point it would want to inject itself into the class?
This is a software development concept named Inversion of control. It decouples classes from another, because there's no need to change existing code when you're changing the service. The service takes care of this itself.
Let's say we simply want to execute logging on every method in every class that starts with delete, like deleteUser()
, deleteEntry()
or whatever. A convenient way would be to write a rule into the logging service and let the framework take care of the correct execution of the code. This is remarkably easy in FLOW3:
/** * An aspect that is executed on all methods that start with "delete". * * @FLOW3\Before("method(.*->delete.*())") */ public function logginMethod() { // Do some logging here. }
FLOW3 performs some really advanced PHP techniques under the hood.
The rule is named pointcut in AOP and there are some easy-to-learn ways to define them. The one above tells FLOW3 to inject the logging code in every class before every method that starts with delete.
It does so by using the method keyword and by adding wildcards (*) for the class and for parts of the method name. If you're adding this code in an Aspect Class (as we do later), you're good to go. You will be able to access properties of the class that you are targeting as well. The targeted class itself remains untouched. It may not yet be written – if it's added to your app in the future, the rule will then match and the code will be injected.
Maybe you have read the above paragraph twice and are asking yourself: Is this magic? What's the damn trick? - I sure did, when I was first introduced to AOP!
Well, AOP has been around for some time in the Java world. FLOW3 is heavily inspired by the AOP implementation of Spring, a Java Framework. The word was on the street, that the pattern can't be transferred to interpreted languages, because it requires compilation.
The trick is, that the framework – or some tools like JAspect – intercepts the compilation process (from Java code to bytecode, that can be executed by the Java runtime) and do an analysis of the defined aspects. Whenever a set of rules (aka the pointcut) matches a method, it is weaved directly into the compiled code. Hence, the framework takes care of merging the code parts together:
FLOW3 did adapt that concept to the world of PHP by introducing an advanced cache. While most frameworks offer caching to increase performance, FLOW3 additionally uses the cache to weave aspects into the original classes.
If you are running your app, FLOW3 delivers the cached classes instead the ones you've written. Hence, your original code remains untouched.
This sounds complex – and it is: FLOW3 performs some really advanced PHP techniques under the hood, that are well worth to be explored! But: To you as an application developer, it's not complicated at all. You don't have to care, if you don't want to: FLOW3 monitors your files for changes and if you make some, FLOW3 auto-commits them to the cache upon your next request. If an aspect matches to a method, it's automatically weaved in – no further configuration needed.
It's a complex pattern conveniently at your service!
A Few Words About the Cache
Every cool feature comes with a downside – so does FLOW3. At the moment, we just scratched the surface of what FLOW3's caching-mechanism is capable of, but I want to give you an idea.
Take a look at the following code:
/** * @FLOW3\Inject * @var \YourApp\MailerServiceInterface */ protected $mailer; /** * Sends a mail. * * @param \YourApp\Model\User $user * @param string $message */ protected function sendMail(\YourApp\Model\User $user, $message) { // Sends a mail. }
The consequence is that PHP is to some degree no longer an interpreted language.
This may look like some code that is just well documented. But it's much more.
The first property annotation (FLOW3\Inject) tells FLOW3 to look for some class in your app that implements the MailerServiceInterface and injects it here (and maybe configure it before). This pattern is known as Dependency Injection.
The second class defines some type definitions in its comment's annotations - and FLOW3 uses them. If you're giving the method some data that can be used to build a User object of your model (for example an array with pure strings from a HTML form), FLOW3 auto-transforms the data safely to a User object and applies some security and validation rules to the second parameter (in this case a string).
This is (among some other tricks) done by weaving in some code into the cache.
The consequence is that PHP is to some degree no longer an interpreted language. The process of caching is somewhat similar to compiling and it has the same downsides: You can't just save & refresh your browser, you have to wait for the cache to be warmed up during development. If you are committing code to your production environment, you have to flush the cache in order to see the changes.
On top of that, FLOW3 adds some kind of type-validation at runtime: You can't pass an integer as second argument in the example above – it'll throw an error. I really like this, because I have a background in strictly typed languages like Java and ActionScript. It also adds a lot of security to your application. But if you're a PHP purist, this may be something that you don't want.
Setting Up Flow3
Alright, enough with theory. Let's get our hands dirty.
Setting up FLOW3 is pretty straightforward, if you already have a PHP-capable server like XAMP or MAMP installed. Just grab a copy of the framework from FLOW3's download site (or clone from git) and unpack it into your htdocs directory.
You should already be done now on Windows machines. Unix-based systems like MAC or Linux need to grant the required rights for building the cache. FLOW3 ships with a powerful command line tool, that packs this task into a one-liner:
# On linux: ./flow3 core:setfilepermissions chris www-data www-data # On Mac: ./flow3 core:setfilepermissions chris _www _www
Please replace chris with your username! This should be enough. However, I've installed FLOW3 on
various machines and have run into some problems. The most common ones are:
- If you're on MAC and FLOW3 complains “index.php not found “, open the .htaccess in the webdir and add a # before the line Rewrite Base /“–
- If you're on Windows and the command line tool does not work, open Settings.yaml.example in FLOW3's configuration directory, uncomment and adjust the phpBinaryPathAndFilename variable to the correct path (e.g. C:\xampp\php\php.exe) and save the file as Settings.yaml
- On some machines, if FLOW3 is very slow, you have to increase the memory limit of PHP in the php.ini
If you run into problems, that are not listed here, please post them in the comments!
Now fire up your favorite browser and navigate to http://localhost/flow3/Web! FLOW3 should welcome you with an introduction screen:
This is the Welcome Package. Every application in FLOW3 (and FLOW3 itself) is a package and packages can share code with each other. For example, once the new TYPO3 Version is finished, you can add it as a package and your application will then have an enterprise-ready CMS on top!
Let's start our own package. It's an easy task, because FLOW3's command line tool will handle most of the work! Type in the following command:
# On linux / MAC ./flow3 kickstart:package Nettuts.AspectDemo # On Windows flow3 kickstart:package Nettuts.AspectDemo
This will create a ready-to-go package named Nettuts.AspectDemo
in FLOW3's Packages/Application folder. If you review the file structure, you will find many folders that do follow a specific convention.
The important ones are:
- Classes: The home of your PHP Code, a first Controller is already created in the Classes/Controller Folder: the StandardController!
- Resources/Private: The home of all templates that are fired into Fluid, FLOW3's powerful Templating Engine. It hosts some other stuff like localization files as well.
- Resources/Public: This has not been created yet, but every file in the Resources/Public folder will be directly accessible from the web. This is a good place for images, CSS, Javascript etc.
Building a full-featured app is beyond the scope of this tutorial, so we'll go with the defaults. Please open the following URL: http://localhost/flow3/Web/index.php/Nettuts.AspectDemo
This is not much, but it's a start. We want to write something to a logfile, every time this site is called. We do this by weaving in an aspect into the StandardController!
Writing a First Aspect
Take a look at the StandardController in theClasses/Controller Folder in your Nettuts.AspectDemo
Package.
It implements a method, named indexAction()
. FLOW3 has automatically configured our app in a way, that this method inside the StandardController gets called when we enter the URL above.
For the moment, we see that this method assigns some strings to the view:
/** * Index action * * @return void */ public function indexAction() { $this->view->assign('foos', array( 'bar', 'baz' )); }
If you open the file index.html of this view in Resources/Private/Templates/Standard inside our package, you'll see how these strings are processed by the templating engine.
However, our goal is to intercept the code execution before this method gets called.
At first, create an empty file named Access.log in FLOW3's Log folder at flow3/Data/Logs. You may save this file wherever you want, but this is a nice place for log files.
Next, create a folder named Service in your Nettuts.AspectDemo/Classes Folder and inside of it, create another folder, and name it Logging. Then create a PHP file named LoggingAspect.php inside of it. It should have the following content:
<?php namespace Nettuts\AspectDemo\Service\Logging; use TYPO3\FLOW3\Annotations as FLOW3; /** * @FLOW3\Aspect */ class LoggingAspect { /** * Log a message if this site is called. * * @param \TYPO3\FLOW3\AOP\JoinPointInterface $joinPoint * @FLOW3\Before("method(Nettuts\AspectDemo\Controller\StandardController->indexAction())") * @return void */ public function logSiteAccess(\TYPO3\FLOW3\AOP\JoinPointInterface $joinPoint) { $filePath = '/home/chris/www/flow3/Data/Logs/Access.log'; $message = 'The site has been accessed at ' . time() . '.' . PHP_EOL; file_put_contents($filePath, $message, FILE_APPEND); } } ?>
This is a very basic class, and if you'd like to have an advanced logger, you may want to separate the logging from the intercepting aspect.
What's going on here? At first, we define a namespace, which is your package name and the path to the actual class (without the class folder itself). Next, we import FLOW3's annotation namespace. Thus, we can use FLOW3's powerful annotation parser to configure our class.
That's exactly what we do in the class comments. We define the class as aspect with the following annotation:
/** * @FLOW3\Aspect */
From now on, FLOW3 is aware that this class may define rules for the interception of the code flow and watches our codebase for matches.
This is the rule:
@FLOW3\Before("method(Nettuts\AspectDemo\Controller\StandardController->indexAction())")
We could use regular expressions or some other techniques to match multiple methods or classes, but we know exactly where we want to go: Before the indexAction()
Method in the StandardController – and that's exactly what we're telling FLOW3.
Note the $jointInterface
Parameter of the logSiteAccess()
Method, that is provided to us by FLOW3. We don't use it here, but it contains some useful data about the actual context. For example, if the intercepted method would have arguments, that we may want to use, we could access them like this:
$argument = $joinPoint->getMethodArgument('argumentName');
The code in our logSiteAccess()
method, itself, is fairly simple. Just adjust the path to the Access.log to your environment and you're good to go!
Now, again, open http://localhost/flow3/Web/index.php/Nettuts.AspectDemo
– nothing should have changed. But when you open the Access.log file, you should see the following entry:
The site has been accessed at 1335715244.
If you can read this, the aspect has been weaved into the cache. Let's have a look at the magic!
FLOW3 stores the cache at flow3/Data/Temporary/Development/Cache/Code/FLOW3_Object_Classes
.
You will find a class named Nettuts_AspectDemo_Controller_StandardController_Original.php
. This is the original class from our codebase.
But there is another class: Nettuts_AspectDemo_Controller_StandardController.php
. This class extends our original class and it is build up purely by generated code. Among the many lines you'll find something like this:
$this->FLOW3_Aop_Proxy_targetMethodsAndGroupedAdvices = array( 'indexAction' => array( 'TYPO3\FLOW3\Aop\Advice\BeforeAdvice' => array( new \TYPO3\FLOW3\Aop\Advice\BeforeAdvice( 'Nettuts\AspectDemo\Service\Logging\LoggingAspect', 'logSiteAccess' $objectManager, NULL), ), ), );
This is were FLOW3 initiates the aspect and it's weaved in directly into your class. The aspect gets executed right where we want it – without touching the original controller.
The code snippet initiates an instance of the BeforeAdvice
class, with our aspect, the respective method and the $objectManager
as arguments. The object manager contains the needed information to build the jointInterface
, that is passed to the aspect.
If you're interested in the details, have a look at the AOP Folder in the FLOW3 package!
Conclusion
The aspect oriented approach has many advantages waiting to be discovered.
The aspect oriented approach has many advantages waiting to be discovered. For example, FLOW3 uses AOP to intercept its own bootstrap to build up a firewall if you choose to use FLOW3's security framework. Apart from that, we haven't yet discussed the specific terminology that is commonly used in AOP.
However, FLOW3 ships with some other unique concepts like Domain-Driven-Design. It has a powerful Doctrine integration and a smart Templating Engine. We have only seen the tip of the iceberg!
What's your opinion? Are you eager to learn more about the philosophy behind FLOW3? What's your impression of AOP? Are you excited to have a robust port for the PHP world or do you think it's too much overhead for an interpreted language?
Comments