Thumbnail Schalpoen code overhaul

Door: Thijs Zumbrink
25-10-2016 12:41

A few days ago Schalpoen became six! I decided to give some care to it again by bringing it into this decade. Not only did the layout change but also the internals.

A quick overview of the changes:
• Most obviously: Material Design (finally responsive!)
• Infrastructure changes as per the previous article
• Separate view and control logic
• Separate persistence and domain logic

Material Design

I've always wanted the site to look clean with as little elements in the way of actual content. That's the reason for the previous (non-) design! However, it was time to enter the modern era, and Material Design fits my needs.

Schalpoen now uses the Materialize CSS framework. Those familiar with Bootstrap will recognize various of its components, most notably the responsive grid system. This means that among the improved design, the site is now also usable on mobile.

For most customizability, I include the SCSS source into my SCSS style sheet. This makes it extremely easy to configure colors, extend existing styles and promote code reuse. It also means I can use just the components I need, instead of the entire framework. Currently I use Netbeans to watch my SCSS folder and compile any changes into the CSS folder that is served by the web server. Eventually this will (also) be done by the build process, to ensure portability.

Infrastructure changes

All changes of the previous post were done. However, there were some future goals mentioned at the end too. First of all, I ran everything through a PSR-2 code formatter. I then namespaced all classes, scanned all includes/requires and replaced them with autoloading. This made everything much easier to work with, since the IDE now knows the project structure.

View separation

All old code was mixed PHP and HTML. The good news was that the project was reasonably object-oriented, but the bad news was that an object knew everything about itself: besides the domain logic it could interact with the database and it could render itself using PHP code, to HTML and RSS. To make it worse, an object's HTML rendering had multiple versions if it could appear on multiple pages. When I first started the design overhaul I stumbled hard over this, since it was too chaotic and all built as a concatenated PHP string with all associated headaches.

So the overhaul began. I took inspiration from Yii2's View component to create my own tiny version. It has a renderPartial($template, $parameters) function to render a piece of HTML using a PHP template, and a render($template, $parameters) function to do the same but embedded into a full layout. (The <html>, <head> etc.)

/**
* Render full page including layout
* @param string $template Relative template file path and name
* @param array $templateParams Parameters provided to the template
* @return string
*/
public function render($template, array $templateParams = [])
{
// $content is used by the layout template, even though it seems unused here!
$content = $this->renderPartial($template, $templateParams);

ob_start();
require($this->layout);
return ob_get_clean();
}

/**
* Render template only without layout
* @param string $template Relative template file path and name
* @param array $templateParams Parameters provided to the template
* @return string
*/
public function renderPartial($template, array $templateParams = [])
{
extract($templateParams);

ob_start();
require($this->viewPath . $template);
return ob_get_clean();
}


This implementation is effectively 10 lines of code only, but changes everything. All view code is now represented in a template, where PHP is the "guest" instead of the "host". The main language is HTML, with PHP code embedded in tiny snippets. This enables the IDE to help with CSS classes and HTML completion, but more importantly, it conceptually groups everything view-related together in a separate subdirectory. Changing the design only means touching these files, so you have the confidence of not missing anything.

Persistence separation

So this leaves the control code and object's domain logic free of presentation logic. But still a big nuisance is the database access dominating the code. Originally every database entity was represented by a class that would take its ID in the constructor and retrieve itself from the database. So retrieving a post was done via $post = new Post(3); All updates were controlled via class methods that would directly write to the database. There were also class methods with domain logic, like those controlling the various states an article can be in and how it can transition those, or how to get its canonical URL.

While using Yii2, I got used to ActiveRecord since we used it a lot. It's a nice tool for building up your project quickly, but after a while I stumbled on a frustration. If you don't pay attention, your Yii2 ActiveRecord can accumulate all kinds of code turning it into a monolithic monster. It takes care of domain logic (validation rules, behaviours and anything custom), presentation (attribute names), persistence (table name, inherited CRUD methods and getters/setters to convert data types) and relations which also have a strong tie to persistence. Yikes, that's a lot!

When I noticed my ActiveRecords getting too fat, I would usually split them into a base class (persistence, validation rules, presentation, relations) while implementing the domain logic in a child class. Still the problem of too many responsibilities stayed: while my class only contained a tiny bit of domain logic, typing $post-> into the editor would get me an enormous list of properties, attributes and methods.

Object repositories

Since I had a clean slate I decided to take another approach. Inspired by Scala Slick (but the idea most definitely exists in many other places) I decided to go for the "Repository approach". In this approach, the domain objects such as Post are "POPO's" (Plain Old PHP Objects), which you can write however you want. They expose their attributes in a clean way that is totally up to the programmer, and they have nothing at all to do with persistence. This class is complemented by a repository (e.g. PostRepository) to control interaction with the database. A code snippet:

$post = PostRepository::retrieve($id);
$post->publish(); // Purely domain logic
PostRepository::update($post);

(Disclaimer: a service locator or DI container might be a good idea for decoupling.)

This might seem a bit clunkier than the ActiveRecord approach (and it definitely doesn't provide its convenient relations) but it does multiple important things: first it keeps the domain objects extremely clean, second it puts all database code in one place, and third it makes all database interaction explicit. There are no accidental "magic" database calls anymore since the code is so simple. Since domain objects purely focus on the domain logic, it becomes easier to test as well.

Any more sophisticated retrieval functions are also implemented in the Repository classes. The Repository takes care that plain data, such as a DB timestamp, is properly populated into the domain objects, which might use DateTimeImmutable. The only downside I have encountered so far is not to have the convenient automatic relations; if you want to JOIN tables, it has to be either queried separately or explicitly built into a repository class. To solve it I will probably again look how Scala Slick handles things.

Conclusion

As the Single Responsibility Principle says, "A class should have only one reason to change." In the old situation, my classes had to change due to the MySQL to PostgreSQL change and due to the Material Design, on top of any domain logic changes. There is still plenty to do (tests, routing, controllers) but at the very least the project is ready for another six years!

Reacties
Log in of registreer om reacties te plaatsen.