Reinventing a PHP MVC framework, part 4

Putting the parts together

This is the fourth part of a series of articles about the mt-mvc PHP MVC framework. If you haven’t read the first parts, here is the first one: Reinventing a PHP MVC framework, part 1

The ASP.NET MVC framework started out pretty simple, but now contains loads of features that I don’t really feel belong in something called an MVC framework. There is stuff like script and stylesheet bundling and minification, helper methods for HTML “controls”, the Action Filters from MVC 3, and so on.

These things are nice to have, but they bloat the framework and should be added using some smart Dependency Injection framework instead.

That is why, in my PHP MVC implementation, I will stick (for now) with the most basic things needed to get some real use out of an MVC framework:

  • Routing URLs matching a pre-set pattern to Controller class names and Views
  • Getting user input from the URL or POST data without hassle
  • Rendering an HTML view
  • Robust, unit-tested code

Url rewriting

To be able to use urls like this one: http://example.com/ninjas/item/52 in PHP, you need to do some url rewriting. This section in .htaccess should work fine for you:

<IfModule mod_rewrite.c> RewriteEngine on RewriteRule ^(.*) mvc.php?path=$1 [L] </IfModule>

Or this setting in IIS UrlRewrite web.config:

<?xml version="1.0" encoding="UTF-8"?> <configuration> <system.webServer> <rewrite> <rules> <rule name="MVC routing" stopProcessing="true"> <match url="^(.*)" ignoreCase="false" /> <action type="Rewrite" url="mvc.php?path={R:1}" /> </rule> </rules> </rewrite> </system.webServer> </configuration>

Then getting the requested path is as simple as calling:

$requestedPath = $_GET['path'];

Autoloading magic

Taking a convention over configuration approach, I simply decide that controller classes should belong to the Controllers namespace, reside in the ~/Controllers/ directory, and have a name ending with Controller. This is in line with how ASP.NET MVC expects things, in the default setting.

By registering an autoload function, I get a certain amount of control over what .php files to include when some part of the solution wants to create an instance of a controller class. The spl_autoload_register function takes a callback function that gets called every time an unknown class is referenced. The callback function can either create the class (probably by including some php file containing the class) and return true, or decide that it is not the right callback for the job, and return false.

The autoloader for MVC controller classes looks like this:

spl_autoload_register(function ($fullClassName) { // Must be Namespace\Classname $parts = explode('\\', $fullClassName); $isTwoParts = (count($parts) == 2); if (!isTwoParts) return false; // Namespace must be 'Controllers' $namespaceName = $parts[0]; $isControllersNamespace = ($namespaceName == 'Controllers'); if (!isControllersNamespace) return false; // Class name must end with 'Controller' $className = $parts[1]; $isControllerSuffix = (substr($className, -10) == 'Controller'); if (!isControllerSuffix) return false; // Look for file here: DOCUMENT_ROOT/Controllers/classname.php $filename = $_SERVER['DOCUMENT_ROOT'] . DIRECTORY_SEPARATOR . 'Controllers' . DIRECTORY_SEPARATOR . $className . '.php'; // Does the file exist? if (!file_exists($filename)) return false; // Include the file. Done! require_once $filename; return true; });

Now that the routing mechanism is (almost) in place, and there is a way of locating the controller classes, putting things together will look something like this:

$routing = new Routing(); $route = $routing->handle($requestedPath); $controllerClassName = 'Controllers\\' . $route->controllerClassName; if (class_exists($controllerClassName)) { $controllerClass = new ReflectionClass($controllerClassName); if ($controllerClass->hasMethod($route->methodName)) { $controllerInstance = new $controllerClassName; $method = $controllerClass->getMethod($route->methodName); $inputModelBuilder = new InputModelBuilder; // TODO: Create $request instance first $inputModel = $inputModelBuilder->buildInputModel($method, $request, $route); $result = $method->invokeArgs($controllerInstance, $inputModel); // TODO: Create $response and $viewRootDir instances first $result->executeResult($response, $viewRootDir); } else { // Non-existing method! // Respond with 404 Not Found } } else { // Non-existing controller! // Respond with 404 Not Found }

Still to do

There is still some code to write before this framework is useful. As you have noticed, some code was mocked in the unit tests of the earlier articles of this series:

  • Some Request class that contains POST data (and possibly other things in the future)
  • Some Response class that knows how to set HTTP headers (and possibly other things in the future)
  • Some FileSystem class that wraps files, directories and that can include files into the output stream
  • Some AutoLoader that loads the different parts of the framework when needed
  • Some ActionResult base class that ViewResult and other result classes can inherit from
  • Some NotFoundResult class that sets the HTTP response to 404
  • Consider putting the framework classes in a namespace of their own

These action points will be the topic of the next article(s). For now, take care!

Reinventing a PHP MVC framework, part 1
Reinventing a PHP MVC framework, part 2
Reinventing a PHP MVC framework, part 3
Reinventing a PHP MVC framework, part 4 (this part)

You’ll find the code from this article in the related release on GitHub. The latest version is always available in the GitHub repository.

Reinventing a PHP MVC framework, part 3

Let’s add some spokes to the wheel

This is the third part of a series of articles about the mt-mvc PHP MVC framework. If you haven’t read the first parts, here is the first one: Reinventing a PHP MVC framework, part 1

When an HTTP request is handled by ASP.NET, a factory called DefaultControllerFactory goes to work. Its CreateController method looks through the web app’s controller classes using reflection, finds the right one and creates an instance. Then the infrastructure takes a look at which method to call, again using reflection, and the method gets called. The result, some ActionResult subclass, then produces the correct view.

The mechanism relies heavily on reflection which is really difficult to unit-test, because you would have to mock the entire class system and file system. So this part is not developed using TDD, but write-and-debug. I’m sorry!

PHP doesn’t have precompiled assemblies that the infrastructure can search through to find the correct class. Instead we need to take a convention over configuration approach, and pick some standards to enforce. Using PHP’s autoload capabilities, we can write an autoloader specifically for controllers. The “controller factory” then simply turns into a call to class_exists. The autoloader is detailed in a future article.

Target in sight

The next piece of the puzzle is what I would like to call the MVC framework itself – a class that binds request, routing and response together. The request and response are mocked, and the workflow looks a little like this:

  1. Get path of url from request handler
  2. Translate path into route information using the routing system
  3. Check the method signature of the method requested:
    • If the method requires a single piece of trivial input, and the parameter is called $id, first try the parameter value from the route information
    • If the method requires additional or non-trivial input, get post data from request handler, using prefixed or non-prefixed keys
    • Build the required input model, if any
    • Because the input model will get passed to a method, the input model is an array of arguments in correct order for passing to the method
  4. Run the method, passing the input model as arguments
  5. Execute the result using the response handler

The routing bit is already in place, albeit not complete for non-trivial production purposes. The next step is creating an input model builder, and here are the first set of tests for it:

class InputModelBuilderTests { public function ReturnsNullForMethodWithoutParameters() { $class = new \ReflectionClass(ImbtControllerDummy::class); $method = $class->getMethod('NoParametersMethod'); // No expected calls to request or route! Expect($route); Expect($request); $builder = new InputModelBuilder(); $result = $builder->buildInputModel($method, $request, $route); The($result)->ShouldEqual([]); } public function ReturnsRouteParameterForSimpleParameterMethod() { $class = new \ReflectionClass(ImbtControllerDummy::class); $method = $class->getMethod('SimpleParameterMethod'); // No expected calls to request! Expect($route)->toGet('parameter', 'abc.123'); Expect($request); $builder = new InputModelBuilder(); $result = $builder->buildInputModel($method, $request, $route); $route->checkAll(); $request->checkAll(); The($result)->ShouldEqual(['abc.123']); } public function ReturnsPostIdForSimpleParameterMethod() { $class = new \ReflectionClass(ImbtControllerDummy::class); $method = $class->getMethod('SimpleParameterMethod'); Expect($route)->toGet('parameter', null); Expect($request)->toGet('post', ['id' => 'abc.123']); $builder = new InputModelBuilder(); $result = $builder->buildInputModel($method, $request, $route); $route->checkAll(); $request->checkAll(); The($result)->ShouldEqual(['abc.123']); } public function ReturnsPostDataForTwoSimpleParametersMethod() { $class = new \ReflectionClass(ImbtControllerDummy::class); $method = $class->getMethod('TwoSimpleParametersMethod'); // No expected calls to route! Expect($route); Expect($request)->toGet('post', ['bar' => 'QWER', 'foo' => 'ASDF']); $builder = new InputModelBuilder(); $result = $builder->buildInputModel($method, $request, $route); $route->checkAll(); $request->checkAll(); The($result)->ShouldEqual(['ASDF', 'QWER']); } public function ReturnsNonprefixedPostDataForComplexParameterMethod() { $class = new \ReflectionClass(ImbtControllerDummy::class); $method = $class->getMethod('SmallInputModelMethod'); // No expected calls to route! Expect($route); Expect($request)->toGet('post', ['bar' => 'QWER', 'foo' => 'ASDF']); $builder = new InputModelBuilder(); $result = $builder->buildInputModel($method, $request, $route); $route->checkAll(); $request->checkAll(); The(count($result))->ShouldBeExactly(1); The($result[0])->ShouldBeInstanceOf(\ImbtSmallInputModel::class); The($result[0]->foo)->ShouldEqual('ASDF'); The($result[0]->bar)->ShouldEqual('QWER'); } public function ReturnsPrefixedPostDataForComplexParameterMethod() { $class = new \ReflectionClass(ImbtControllerDummy::class); $method = $class->getMethod('SmallInputModelMethod'); // No expected calls to route! Expect($route); Expect($request)->toGet('post', ['model-bar' => 'QWER', 'model-foo' => 'ASDF']); $builder = new InputModelBuilder(); $result = $builder->buildInputModel($method, $request, $route); $route->checkAll(); $request->checkAll(); The(count($result))->ShouldBeExactly(1); The($result[0])->ShouldBeInstanceOf(\ImbtSmallInputModel::class); The($result[0]->foo)->ShouldEqual('ASDF'); The($result[0]->bar)->ShouldEqual('QWER'); } } class ImbtControllerDummy { public function NoParametersMethod() { } public function SimpleParameterMethod($id) { } public function TwoSimpleParametersMethod($foo, $bar) { } public function SmallInputModelMethod(ImbtSmallInputModel $model) { } } class ImbtSmallInputModel { public $foo; public $bar; }

Making these tests succeed is not that hard, but I have to admit that we have strayed a little from the ASP.NET path now. This is intentional. I want the ease-of-use of ASP.NET, but also want to add some ideas of my own.

class InputModelBuilder { public function buildInputModel(ReflectionMethod $method, $request, $route) { $parameters = $method->getParameters(); $parameterCount = count($parameters); if ($parameterCount === 0) { return []; } $result = []; foreach ($parameters as $index => $parameter) { $typeHint = $parameter->getClass(); $name = $parameter->getName(); // Trivial single-value input model named $id: if ($name == 'id' && !isset($typeHint) && $parameterCount === 1) { // This is the only time the $route->parameter is used! $value = @$route->parameter; if (isset($value)) { return[$value]; } } if (!isset($postData)) $postData = $request->post; if (!isset($typeHint)) { // Trivial single value from post if (isset($postData[$name])) { $result[] = $postData[$name]; } } else { // Type-hinted value $result[] = $this->buildTypeHintedObject($name, $typeHint, $postData); } } return $result; } private function buildTypeHintedObject($optionalPrefix, ReflectionClass $typeHint, array $postData) { $className = $typeHint->getName(); $result = new $className; $properties = $typeHint->getProperties(); foreach ($properties as $property) { $name = $property->getName(); $prefixedName = "$optionalPrefix-$name"; if (isset($postData[$prefixedName])) { $property->setValue($result, $postData[$prefixedName]); } else if (isset($postData[$name])) { $property->setValue($result, $postData[$name]); } } return $result; } }

The bits and pieces are starting to fall into place, but they all need to be put together. Come back here in a while for more on that.

Reinventing a PHP MVC framework, part 1
Reinventing a PHP MVC framework, part 2
Reinventing a PHP MVC framework, part 3 (this part)

You’ll find the code from this article in the related release on GitHub. The latest version is always available in the GitHub repository.

Trying out Visual Studio Code

Yesterday Microsoft held Build 2015 and presented lots of nice things to the developer community. I have always loved Microsoft’s developer tools, and started using Visual Studio back in 1997, hacking away with Visual Basic and Visual C++.

Now I still use Visual Studio at work, but for my personal projects, I mostly do web development in PHP and sometimes ASP.NET MVC. For the PHP projects, I have been using Komodo Edit for a while now, and am happy with it.

Visual Studio Code

installing-ms-code
After a quick download and a smooth installation, Visual Studio Code booted. I opened a folder full of PHP and JavaScript files. Syntax highlighting, bracket matching, syntax errors and warnings work really well for PHP, JavaScript, HTML and CSS, but PHP IntelliSense isn’t included in this Preview version, which unfortunately means I won’t be switching. Yet. However, JavaScript IntelliSense is amazing! It found a rookie mistake for me…
Don't do bitwise operations on bool

IntelliSense in Visual Studio Code is really good in every language it supports. Code completion and suggestions for JavaScript, HTML, CSS, SASS and C# are instant, but some features are missing from HTML IntelliSense.

A couple of HTML IntelliSense suggestions

  • Allowed attribute values should be suggested, like when typing <link rel=", I would like a popdown list to suggest things like stylesheet and so on.
  • Element suggestion should only include elements that make sense in the context. Directly inside an <ul> element, there is no point in suggesting a <blockquote>. Only <li>, <script> and <template> elements make any sense.

Bad elements in UL

What will make me switch

The editor is really nice to work with, it feels snappy and does things well. Changing personal settings is done in JSON, which is cool, because JSON… Until PHP IntelliSense is added, and some improvements are made in HTML editing, I will stick to Komodo Edit, but I will probably switch to Visual Studio Code eventually.

Reinventing a PHP MVC framework, part 2

Let’s make the wheel more round

This is the second part of a series of articles about the mt-mvc PHP MVC framework. If you haven’t read the first part, here it is: Reinventing a PHP MVC framework, part 1

Return to sender

In the old days, before fire was invented, responding to a request was done by calling Response.Write and writing directly to the response stream. In an MVC world (in whatever language, but especially in an object-oriented one), doing this from within a controller is a big no-no. Writing to the stream, using Response.Write or echo will only happen in the View!

In ASP.NET MVC, responding to an HTTP request is done by returning an instance of a class derived from the abstract ActionResult class. For a normal page view, you return a ViewResult object. For an AJAX request expecting JSON data, you return a JsonResult object. Some other examples are the RedirectResult, HttpStatusCodeResult, AtomFeedActionResult, and FileContentResult classes.

Most of those classes reference some model object, and will eventually render something view-like using the properties of the model object. The rendering itself, including sending HTTP headers, takes place in an implementation of the abstract ExecuteResult method. For now, I will focus only on serving ordinary views, like ASP.NET MVC does through the ViewResult class.

Some assembly needed

Using the Routing class from the previous part, we can find the names of a controller class and the method to call. We will now expect that method to return an object that has an executeResult method (first letter is lower-case, because PHP). I actually want to make my MVC framework act more in line with the MVC pattern than ASP.NET.

First of all, I don’t want the controller to be able to access response artefacts like HTTP response headers, and the response content stream, because those are definitely presentation details. To have a clear separation of duties, those things should only be available to the View. Because of this, the executeResult method needs to be provided with some mechanism for setting HTTP headers and writing content. This “response wrapper” is easily mocked, for now. For testability, we also need to mock the filesystem.

This first iteration of ViewResult should set the Content-Type to text/html and then perform a standard PHP include on a view php file, using a (mocked) filesystem wrapper.

class ViewResultTests { public function ExecuteResultSetsCorrectContentType() { $controllerName = 'Home'; $viewName = 'Index'; $model = null; $viewResult = new ViewResult($controllerName, $viewName, $model); Expect($response)->toCall('setHeader')->withArguments(['Content-Type', 'text/html; charset=utf-8']); Expect($viewRootDir)->toCall('phpInclude')->withArguments(['home/index.php']); $viewResult->executeResult($response, $viewRootDir); $response->checkAll(); $viewRootDir->checkAll(); } } class ViewResult { private $controllerName; private $viewName; public function __construct($controllerName, $viewName, $model) { $this->controllerName = $controllerName; $this->viewName = $viewName; } public function executeResult($response, $viewRootDir) { $response->setHeader('Content-Type', 'text/html; charset=utf-8'); $viewRootDir->phpInclude(mb_strtolower($this->controllerName) . '/' . mb_strtolower($this->viewName) . '.php'); } }

The constructor for the ViewResult class needs the name of the controller, not the controller class. For this, we need to add a few more lines to the RoutingTests and Routing classes. That code is trivial and out of scope for this article, but you can look at it in the GitHub release.

All parts

Reinventing a PHP MVC framework, part 1
Reinventing a PHP MVC framework, part 2 (this part)

You’ll find the code from this article in the related release on GitHub. The latest version is always available in the GitHub repository.

Reinventing a PHP MVC framework, part 1

Let’s reinvent the wheel

This is the first part of a series of articles about the mt-mvc PHP MVC framework.

I wanted to know how ASP.NET MVC does what it does, so I decided to find out… by trying to reinvent it… in PHP. My line of thought was this:

  • I know how to USE the ASP.NET MVC framework
  • I know the effects of using the various features of the ASP.NET MVC framework
  • I know the principles of TDD
  • I should be able to reinvent (or reverse-engineer) a working MVC framework by adding unit tests for increasingly complex use of MVC, and making one or a few tests pass at a time
  • I also want to become a better PHP developer

I am perfectly aware of the fact that there are lots of MVC frameworks for PHP that are really capable of taking care of business, but this is not a website development effort. This is a learning effort. Reinventing the wheel works fine for learning – not for production code.

MVC the ASP.NET way

Let’s start with something simple. The most basic use of ASP.NET MVC, in the default setting, appears to work by separating the request path of an incoming request into a Controller class name, a View method name, and an optional parameter value that gets passed into the method. Also, there are default values for all parts of the path.

First set of tests

I imagine a class that’s solely responsible for parsing a path, and suggesting the name of a controller class, and a method to call, so I write some tests for that class first. Hooking things up to the PHP HTTP infrastructure gets added later.

class RoutingTests { public function CheckAllDefaults() { $routing = new Routing(); $route = $routing->handle(''); The($route->controllerClassName)->shouldEqual('HomeController'); The($route->methodName)->shouldEqual('Index'); The($route->parameter)->shouldNotBeSet(); } public function CheckDefaultMethodNameAndParameter() { $routing = new Routing(); $route = $routing->handle('Articles'); The($route->controllerClassName)->shouldEqual('ArticlesController'); The($route->methodName)->shouldEqual('Index'); The($route->parameter)->shouldNotBeSet(); } public function CheckDefaultParameter() { $routing = new Routing(); $route = $routing->handle('Categories/List'); The($route->controllerClassName)->shouldEqual('CategoriesController'); The($route->methodName)->shouldEqual('List'); The($route->parameter)->shouldNotBeSet(); } public function CheckNoDefaults() { $routing = new Routing(); $route = $routing->handle('Products/Item/123x'); The($route->controllerClassName)->shouldEqual('ProductsController'); The($route->methodName)->shouldEqual('Item'); The($route->parameter)->shouldEqual('123x'); } }

These tests are about the default out-of-the-box behavior of the routing subsystem. More advanced features, like registering custom url patterns, get added later.

class Routing { public function handle($url) { $parts = explode('/', $url); $controllerName = @$parts[0]; $methodName = @$parts[1]; $parameter = @$parts[2]; if (!$controllerName) $controllerName = 'Home'; if (!$methodName) $methodName = 'Index'; return (object) [ 'controllerClassName' => $controllerName . 'Controller', 'methodName' => $methodName, 'parameter' => $parameter ]; } }

Usefulness right now

This class does the bare minimum, and making some real use of it requires a lot of nuts and bolts in place – some URL redirection, a request/response pipeline system, some use of reflection to dynamically create controller instances and calling methods, a lot of thought about how to connecting views to the controller methods, and so on. Don’t worry; all of that will be covered in the following posts.

All parts

Reinventing a PHP MVC framework, part 1 (this part)
Reinventing a PHP MVC framework, part 2

You’ll find the code from this article in the related release on GitHub. The latest version is always available in the GitHub repository.

About to solve an old THREE.js bug and move on with Artsy

I really need to pay more attention. Almomst a year ago, THREE.js released the r67 version, which removed the concept of centroids. This made part 3 of Artsy break. I used centroids and the Mesh.calculateCentroid function, not because I needed to, but because some tutorial told me I should.

When the concept of centroids was removed, in April 2014, my JavaScript demos was very low on my list of priorities, but soon I will make time for fixing and advancing. Who knows, I might even be able to finish Artsy once and for all. I started working on it in October of 2013, so it’s really overdue!

For now, I have removed the calls to calculateCentroid and done some small changes to at least get Part 3 to start. Stay posted!

First version of GitHub Webhook handler public on GitHub

Yesterday, I wrote about my efforts for creating an easy-to-use GitHub Webhooks handler in PHP, suitable for shared hosting environments.

After a few hours of making the code a little prettier, it is now public on GitHub. I remade the whole thing into an API style that I would enjoy using. Now, you can hook yourself up to GitHub Webhooks like this:

<?php require_once "mt-github-webhook.php"; // Changes in the QA branch are pushed to the secret password-protected QA web site \MT\GitHub\Webhook::onPushToBranch("qa-testing")-> forChangesInFolder("main-web-site/public_html")-> setGitHubCredentials("github-username", "My5ecretP@ssw0rd")-> pushChangesToFolder("/www/sites/qa.domain.com/public_html"); // Changes in the PRODUCTION branch are pushed to the public-facing web site \MT\GitHub\Webhook::onPushToBranch("production")-> forChangesInFolder("main-web-site/public_html")-> setGitHubCredentials("github-username", "My5ecretP@ssw0rd")-> pushChangesToFolder("/www/sites/www.domain.com/public_html"); ?>

The clone url is: https://github.com/lbrtw/mt-github-webhook.git. Feel free to fork and play around with it.

Automatic deployment on shared server using GitHub webhooks

If you, like me, have a few spare time projects, chances are you don’t own or rent a dedicated server for your web hosting. I use Loopia (a Swedish web hosting provider) for my hosting purposes. I use their web hotel service, so I have very little control over file system paths, php modules and such.

On a dedicated server, using GitHub webhooks is pretty straightforward. When your server gets notified of a push or a closed merge request, you can do a simple git clone to create a fresh full copy of the branch you are using for your deploys. On a shared system, without access to the git command-line tools, it gets a little tricker.

I have developed a php-based solution that works for me. My branch and merge setup looks something like this:

  • master : This is the Main development branch
  • dev : This is the Online testing branch
  • vnext : This branch is Used for demonstration purposes, and possibly pilots
  • www : This is the Current stable running version
  • Changes pushed often from master to dev
    • 26 Jan at 16:29: Bug fix
    • 27 Jan at 19:11: New feature
    • 29 Jan at 09:53: Experimenting
    • 30 Jan at 13:49: Bug fix
    • 01 Feb at 11:52: Bug fix
    • 02 Feb at 13:20: Experimenting
    • 03 Feb at 08:41: New feature
    • 04 Feb at 16:17: Bug fix
    • 06 Feb at 11:53: Bug fix
    • 07 Feb at 08:28: New feature
    • 07 Feb at 18:16: Experimenting
    • 09 Feb at 17:32: New feature
    • 10 Feb at 12:53: Bug fix
    • 12 Feb at 12:10: Experimenting
    • 13 Feb at 11:11: New feature
  • Version candidate pushed weekly from dev to vnext
    • 30 Jan at 17:29: Customer demo
    • 05 Feb at 12:52: Internal release
    • 11 Feb at 08:14: Customer demo
  • New version pushed to production when done from vnext to www
    • 07 Feb at 15:49: Live deployment

All development is performed in the master branch. Whenever a feature makes enough progress to be visible or usable (or is completed), or a bug is fixed, I merge to the dev branch. Every now and then, I’m not the only coder making changes. When other coders are done with a feature or a bug-fix, they create a pull request that I approve to perform the merge.

The dev branch is where we test everything internally. We can do experiments, move stuff around, temporarily remove features or add wild and crazy stuff. When the dev branch is good enough for showing to people, we merge to the vnext branch, which is always a little more stable and feels more “done”. This is where customers can check out future features and have their say in stuff.

After a couple of rounds of pushing to vnext, it’s time to go live. This is done by merging to the www branch.

Continuous Integration and Deployment

Every time something gets pushed into the non-master branches, GitHub posts a message to my webhook handler. The handler reads the message payload to find out what files are changes and what branch is the target. Using this information, it downloads the correct source files from raw.githubusercontent.com and copies to the correct directory of the shared web server file system.

// We are only interested in PUSH events for now $eventName = @$_SERVER['HTTP_X_GITHUB_EVENT']; if ($eventName != 'push') { http_response_code(412); exit("This is not a PUSH event. Aborting..."); } // Read and parse the payload $jsonencodedInput = file_get_contents("php:\/\/input"); $inputData = json_decode($jsonencodedInput); // What branch is this? $branchRef = $inputData->ref; // If I'm interested in the branch, copy all changes, otherwise quit if ($branchRef == 'refs/heads/dev') { copyChanges('/WEB-HOTEL-ROOT/dev.domainname.com/', 'dev', $inputData); } else if ($branchRef == 'refs/heads/vnext') { copyChanges('/WEB-HOTEL-ROOT/vnext.domainname.com/', 'vnext', $inputData); } else if ($branchRef == 'refs/heads/www') { copyChanges('/WEB-HOTEL-ROOT/domainname.com/', 'www', $inputData); } else { http_response_code(412); exit("I'm not interested in the $branchRef branch. Aborting..."); }

The code above is simple enough. Depending on the type of event, and on the name of the branch, the script either exits immediately with a nice error message (that you can read in your GitHub repository’s webhook settings page), or calls the copyChanges function, shown below.

function copyChanges($rootFolder, $branchName, $inputData) { // Check all commits involved in this push for changes that I'm interested in $interestingChanges = extractInterestingChangesFromCommits($inputData->commits); $changedPaths = array_keys($interestingChanges); // No interesting changes? Quit! if (count($changedPaths) == 0) { exit("No interesting changes. Goodbye!"); } foreach ($changedPaths as $localPath) { $fullPath = $rootFolder . $localPath; $changeType = $interestingChanges[$localPath]; if ($changeType == 'delete') { // Deleted file - delete it! unlink($fullPath); } else { // Added or modified file - download it! $url = "https://USERNAME:PASSWORD@raw.githubusercontent.com/USERNAME/REPOSITORY/$branchName/$localPath"; $fileContents = file_get_contents($url); if ($fileContents !== false) { file_put_contents($fullPath, $fileContents); } } } }

Actually, the code I use contains some more error checking. It also recursively creates new directories if a file wants to be put in a directory that does not yet exist.

function extractInterestingChangesFromCommits($commits) { // This function returns an array where // the keys are local file paths, and // the values are the type of change // Something like this: // [ // 'path/file.1' => 'add', // 'path/file.2' => 'change', // 'path/file.3' => 'delete' // ] $result = []; foreach ($commits as $commit) { foreach ($commit->added as $added) { $result[$added] = 'add'; } foreach ($commit->modified as $modified) { $result[$modified] = 'change'; } foreach ($commit->deleted as $deleted) { $result[$deleted] = 'delete'; } } return $result; }

That’s about it for now. The script has been running and handling deployments for my spare-time projects for a while now, and I feel confident about it. I’ll make some more touchups to this script, and then I’ll put it on GitHub for you to star. Check in for a link in a few days.

Ain’t nobody got time for WordPress themes written from scratch

This blog has been on life-support for a while now. I have been busy getting married, focusing on my day-job, enjoying life in different ways, and sometimes life is just too full.

Today I removed my old WordPress theme that I wrote from scratch and switched to Twenty Fifteen, with just one addition – my custom code formatter that takes care of making HTML, CSS, JS, C# and PHP looking nice.

Ain’t nobody got time for maintaining WordPress themes written from scratch! But actually, I got time for blogging again. So I will. I promise…