January 15, 2016 by Christoff Truter PHP Architecture
First of all, happy new year and may the force be with you ;)
Last year in the first part of this series of posts we had a quick look at the technologies that makes the frontend tick, in this post we're going to have a look at the backend service used for saving movie metadata.
If you were to go through the change history for this repository, you will notice quite a few changes made to the
backend service. This is greatly influenced by a rather frustrating interview I had with a company in Germany last year.
Admittedly I stunningly dropped the ball, I am also clearly out of touch with the happenings in the PHP community (been focussing more on C#, Objective-C etc the last few years), the frustration however comes in where one of the developers were tasked to review a code assignment and spent a mere five minutes rush one-sided reviewing my mediocre code with a very dogmatical understanding (and a false understanding at that in my opinion) of the application of S.O.L.I.D principles (perhaps as a result of their rush).
But nonetheless, I decided to treat it as a learning opportunity and though discouraged still hopeful that someone, somewhere might see some potential in me and give me a chance to prove myself and escape from South Africa.
Changes
The first change is the introduction of composer into the codebase, over the last few years composer became the de facto dependency manager for PHP (think of it as npm for PHP).
Composer will download the required dependencies (as defined in the composer.json file) and setup auto loading of classes (vendor/autoload.php).
As part of the exercise, I analysed the project to identify reusable code and ended up moving code into three additional projects (each containing its own composer.json file), namely:
Note that these projects are still in their infancy, I only implemented the functionality I needed.
The second notable change is the introduction of doctrine orm into the codebase, apparently which has become the de facto ORM (object relational mapping) for PHP.
I simply added some annotations to my existing models, instantiated an EntityManager connecting to my existing database and implemented the appropriate repository interfaces.
<?php namespace Models; use Doctrine\ORM\Mapping as ORM; /** * @ORM\Entity * @ORM\Table(name="movies") * @ORM\Entity(repositoryClass="Repositories\Movies") */ class Movie { /** * @ORM\Column(type="integer", name="movieId") * @ORM\Id * @ORM\GeneratedValue(strategy="AUTO") */ public $movieId; /** * @ORM\Column(type="string", name="imdbId", length=50) */ public $imdbID;
There are various other doctrine mapping options available as well, like XML, YAML and writing PHP code to map entities, its even possible to auto generate code for your entities.
I must confess though, I am not a huge fan of the ORM technique - discussion for another day...
Build Process
With the introduction of composer and doctrine into the equation, line 5 - 7 was added to the build batch file seen below.
cd app
call npm install
xcopy /S /Y /I ".\node_modules\jquery-mobile\dist\images\*.*" "..\css\images"
call gulp
cd ../Service
call composer install
call vendor/bin/doctrine orm:schema-tool:create
pause
On line 5 the "composer install" command parses / runs the contents of the composer.json file(s).
The interesting part is line 6, this doctrine command will create the required MySQL tables according to the defined annotations, but it won't create the database itself.
I spent a few hours searching the web to find a doctrine specific way to auto create the database as well (seeing that I wanted to get rid of requiring to execute a SQL dump file all together). Unfortunately I couldn't find a doctrine specific way yet, so I ended up doing the following:
<?php include 'bootstrap.php'; use Doctrine\ORM\Tools\Console\ConsoleRunner, Doctrine\ORM\EntityManager; try { $entityManager->getConnection()->connect(); } catch(Exception $ex) { $connection = EntityManager::create([ 'driver' => 'pdo_mysql', 'host' => MYSQL_HOST, 'user' => MYSQL_USERNAME, 'password' => MYSQL_PASSWORD, 'charset' => 'UTF8' ], $config)->getConnection(); $connection->executeUpdate('CREATE DATABASE '.MYSQL_DATABASE.' CHARACTER SET utf8 COLLATE utf8_general_ci'); } return ConsoleRunner::createHelperSet($entityManager); ?>
Note: The doctrine command requires the presence of a cli-config.php file, contents as seen above.
Basically the script attempts to connect to the specified database, if that fails it attempts to create the database (assuming that the user is allowed to perform database creation that is - might be prudent to delete this file
after deployment).
Code overview
Lets jump into some code, observe the snippet below:
<?php use Models\Movie, Repositories\Interfaces\IMovieRepository, CSTruter\Service\Results\JsonResult, CSTruter\Service\Results\BinaryResult, CSTruter\Service\Results\ErrorResult, CSTruter\Facebook\Service\FacebookServiceBase, CSTruter\Misc\Request; class MovieService extends FacebookServiceBase { private $repository; public function __construct(IMovieRepository $repository, $appId, $secret) { $this->repository = $repository; parent::__construct($appId, $secret); }
protected function registerMethods() { $this->registerMethod('POST', 'addMovie') ->requireJson() ->requireValidation(); $this->registerMethod('DELETE', 'removeMovie') ->requiredArguments('movieId') ->requireValidation();
abstract class FacebookServiceBase extends ServiceBase implements ISecureService { private $api; public function __construct($appId, $secret) { $this->api = new FacebookApi($appId, $secret); parent::__construct(); }
public function validate($signed_request = NULL) { try { $json = $this->api->parseSignedRequest($signed_request); if ((!isset($_SESSION['signed_request'])) ||
($_SESSION['signed_request'] != $signed_request)) { $this->api->validateSignedRequest($json['code']); $_SESSION['signed_request'] = $signed_request; } $_SESSION['id'] = $json['user_id'];
Initially when I wrote these methods, I tried a variety of approaches, e.g. I passed the Facebook token to the service via the frontend and validated the token on the backend against Facebook every time. Obviously not the ideal approach, as it will cause a major performance hit while being totally unnecessary, nevermind the Facebook API Rate limit that you will ultimately run into.
I also considered doing one time validation on the token and simply use the session in subsequent calls, but this opened the project to potential security risks (tokens can be reused).
I ended up using the signed request itself, the nice thing about this is, the signed request can only be used once, making it useless to an attacker the second time around. You can have a look at how it practically works in the FacebookApi class.
November 10, 2015