Commit:    f762db348dc0ef698bdbd3cd4c95530b1266e027
Author:    Pieter Hordijk <[email protected]>         Thu, 16 May 2019 
16:05:18 +0300
Committer: Peter Kokot <[email protected]>      Thu, 16 May 2019 22:05:30 
+0200
Parents:   3c825bc8531f1896d823c77eba97b2b709338427
Branches:  master

Link:       
http://git.php.net/?p=web/bugs.git;a=commitdiff;h=f762db348dc0ef698bdbd3cd4c95530b1266e027

Log:
Add tests fixes

- Added filter to phpunit config
- This enables support for coverage support
- Moved unit tests to dedicated directory
- Also created constants for the locations of the fixtures and mocks for
  testing so we do not have to use relative paths in tests
- Updated phpunit to 8
- Added type information to tests
- Added return type and parameter type declarations for tests
- Added strict types to tests
- CS fixes in tests
- Aligning arrays, consistent string concatenation, removed unused local
  vars
- Added constant for test cache directory
- Make the autoloader also properly work on windows
- Windows does not care about the directory separator used so just trim
  both variant from the end.

Changed paths:
  M  .gitignore
  M  composer.json
  M  composer.lock
  M  phpunit.xml.dist
  M  src/Autoloader.php
  D  tests/AutoloaderTest.php
  D  tests/Container/ContainerTest.php
  D  tests/Container/MockDependency.php
  D  tests/Container/MockService.php
  A  tests/Mocks/responses/dev-body.txt
  A  tests/Mocks/responses/stable-body.txt
  D  tests/Template/ContextTest.php
  D  tests/Template/EngineTest.php
  A  tests/Unit/AutoloaderTest.php
  A  tests/Unit/Container/ContainerTest.php
  A  tests/Unit/Container/MockDependency.php
  A  tests/Unit/Container/MockService.php
  A  tests/Unit/Template/ContextTest.php
  A  tests/Unit/Template/EngineTest.php
  A  tests/Unit/Utils/CacheTest.php
  A  tests/Unit/Utils/CaptchaTest.php
  A  tests/Unit/Utils/UploaderTest.php
  A  tests/Unit/Utils/Versions/ClientTest.php
  A  tests/Unit/Utils/Versions/GeneratorTest.php
  D  tests/Utils/CacheTest.php
  D  tests/Utils/CaptchaTest.php
  D  tests/Utils/UploaderTest.php
  D  tests/Utils/Versions/ClientTest.php
  D  tests/Utils/Versions/GeneratorTest.php
  A  tests/bootstrap.php
  D  tests/mock/responses/dev-body.txt
  D  tests/mock/responses/stable-body.txt

diff --git a/.gitignore b/.gitignore
index 07c61bc..8d0441e 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,3 +1,9 @@
+# These files are generated during application running or testing and are
+# intentionally untracked to ignore by Git. For other development environment
+# specific files, such as editor configuration, a good practice is to exclude
+# them using the .git/info/exclude in the cloned repository or a global
+# .gitignore file.
+
 # Uploaded patches
 /uploads/
 
@@ -6,6 +12,7 @@
 
 # Local specific PHPUnit configuration
 /phpunit.xml
+.phpunit.result.cache
 
 # Transient and temporary generated files
 /var/
diff --git a/composer.json b/composer.json
index 438518b..c32eeda 100644
--- a/composer.json
+++ b/composer.json
@@ -31,7 +31,7 @@
         "ext-session": "*"
     },
     "require-dev": {
-        "phpunit/phpunit": "^7.5"
+        "phpunit/phpunit": "^8.1.5"
     },
     "autoload": {
         "psr-4": {
diff --git a/composer.lock b/composer.lock
index b5c6108..63aa327 100644
--- a/composer.lock
+++ b/composer.lock
@@ -4,7 +4,7 @@
         "Read more about it at 
https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies";,
         "This file is @generated automatically"
     ],
-    "content-hash": "c4d7d1e13a174de9ebf8016cf7872528",
+    "content-hash": "dba2ed92613c9c0acb06a57f86464228",
     "packages": [],
     "packages-dev": [
         {
@@ -430,40 +430,40 @@
         },
         {
             "name": "phpunit/php-code-coverage",
-            "version": "6.1.4",
+            "version": "7.0.3",
             "source": {
                 "type": "git",
                 "url": 
"https://github.com/sebastianbergmann/php-code-coverage.git";,
-                "reference": "807e6013b00af69b6c5d9ceb4282d0393dbb9d8d"
+                "reference": "0317a769a81845c390e19684d9ba25d7f6aa4707"
             },
             "dist": {
                 "type": "zip",
-                "url": 
"https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/807e6013b00af69b6c5d9ceb4282d0393dbb9d8d";,
-                "reference": "807e6013b00af69b6c5d9ceb4282d0393dbb9d8d",
+                "url": 
"https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/0317a769a81845c390e19684d9ba25d7f6aa4707";,
+                "reference": "0317a769a81845c390e19684d9ba25d7f6aa4707",
                 "shasum": ""
             },
             "require": {
                 "ext-dom": "*",
                 "ext-xmlwriter": "*",
-                "php": "^7.1",
-                "phpunit/php-file-iterator": "^2.0",
+                "php": "^7.2",
+                "phpunit/php-file-iterator": "^2.0.2",
                 "phpunit/php-text-template": "^1.2.1",
-                "phpunit/php-token-stream": "^3.0",
+                "phpunit/php-token-stream": "^3.0.1",
                 "sebastian/code-unit-reverse-lookup": "^1.0.1",
-                "sebastian/environment": "^3.1 || ^4.0",
+                "sebastian/environment": "^4.1",
                 "sebastian/version": "^2.0.1",
                 "theseer/tokenizer": "^1.1"
             },
             "require-dev": {
-                "phpunit/phpunit": "^7.0"
+                "phpunit/phpunit": "^8.0"
             },
             "suggest": {
-                "ext-xdebug": "^2.6.0"
+                "ext-xdebug": "^2.6.1"
             },
             "type": "library",
             "extra": {
                 "branch-alias": {
-                    "dev-master": "6.1-dev"
+                    "dev-master": "7.0-dev"
                 }
             },
             "autoload": {
@@ -489,7 +489,7 @@
                 "testing",
                 "xunit"
             ],
-            "time": "2018-10-31T16:06:48+00:00"
+            "time": "2019-02-26T07:38:26+00:00"
         },
         {
             "name": "phpunit/php-file-iterator",
@@ -682,16 +682,16 @@
         },
         {
             "name": "phpunit/phpunit",
-            "version": "7.5.9",
+            "version": "8.1.5",
             "source": {
                 "type": "git",
                 "url": "https://github.com/sebastianbergmann/phpunit.git";,
-                "reference": "134669cf0eeac3f79bc7f0c793efbc158bffc160"
+                "reference": "01392d4b5878aa617e8d9bc7a529e5febc8fe956"
             },
             "dist": {
                 "type": "zip",
-                "url": 
"https://api.github.com/repos/sebastianbergmann/phpunit/zipball/134669cf0eeac3f79bc7f0c793efbc158bffc160";,
-                "reference": "134669cf0eeac3f79bc7f0c793efbc158bffc160",
+                "url": 
"https://api.github.com/repos/sebastianbergmann/phpunit/zipball/01392d4b5878aa617e8d9bc7a529e5febc8fe956";,
+                "reference": "01392d4b5878aa617e8d9bc7a529e5febc8fe956",
                 "shasum": ""
             },
             "require": {
@@ -701,27 +701,25 @@
                 "ext-libxml": "*",
                 "ext-mbstring": "*",
                 "ext-xml": "*",
+                "ext-xmlwriter": "*",
                 "myclabs/deep-copy": "^1.7",
                 "phar-io/manifest": "^1.0.2",
                 "phar-io/version": "^2.0",
-                "php": "^7.1",
+                "php": "^7.2",
                 "phpspec/prophecy": "^1.7",
-                "phpunit/php-code-coverage": "^6.0.7",
+                "phpunit/php-code-coverage": "^7.0",
                 "phpunit/php-file-iterator": "^2.0.1",
                 "phpunit/php-text-template": "^1.2.1",
                 "phpunit/php-timer": "^2.1",
                 "sebastian/comparator": "^3.0",
                 "sebastian/diff": "^3.0",
-                "sebastian/environment": "^4.0",
+                "sebastian/environment": "^4.1",
                 "sebastian/exporter": "^3.1",
-                "sebastian/global-state": "^2.0",
+                "sebastian/global-state": "^3.0",
                 "sebastian/object-enumerator": "^3.0.3",
                 "sebastian/resource-operations": "^2.0",
                 "sebastian/version": "^2.0.1"
             },
-            "conflict": {
-                "phpunit/phpunit-mock-objects": "*"
-            },
             "require-dev": {
                 "ext-pdo": "*"
             },
@@ -736,7 +734,7 @@
             "type": "library",
             "extra": {
                 "branch-alias": {
-                    "dev-master": "7.5-dev"
+                    "dev-master": "8.1-dev"
                 }
             },
             "autoload": {
@@ -762,7 +760,7 @@
                 "testing",
                 "xunit"
             ],
-            "time": "2019-04-19T15:50:46+00:00"
+            "time": "2019-05-14T04:57:31+00:00"
         },
         {
             "name": "sebastian/code-unit-reverse-lookup",
@@ -931,16 +929,16 @@
         },
         {
             "name": "sebastian/environment",
-            "version": "4.2.1",
+            "version": "4.2.2",
             "source": {
                 "type": "git",
                 "url": "https://github.com/sebastianbergmann/environment.git";,
-                "reference": "3095910f0f0fb155ac4021fc51a4a7a39ac04e8a"
+                "reference": "f2a2c8e1c97c11ace607a7a667d73d47c19fe404"
             },
             "dist": {
                 "type": "zip",
-                "url": 
"https://api.github.com/repos/sebastianbergmann/environment/zipball/3095910f0f0fb155ac4021fc51a4a7a39ac04e8a";,
-                "reference": "3095910f0f0fb155ac4021fc51a4a7a39ac04e8a",
+                "url": 
"https://api.github.com/repos/sebastianbergmann/environment/zipball/f2a2c8e1c97c11ace607a7a667d73d47c19fe404";,
+                "reference": "f2a2c8e1c97c11ace607a7a667d73d47c19fe404",
                 "shasum": ""
             },
             "require": {
@@ -980,7 +978,7 @@
                 "environment",
                 "hhvm"
             ],
-            "time": "2019-04-25T07:55:20+00:00"
+            "time": "2019-05-05T09:05:15+00:00"
         },
         {
             "name": "sebastian/exporter",
@@ -1051,23 +1049,26 @@
         },
         {
             "name": "sebastian/global-state",
-            "version": "2.0.0",
+            "version": "3.0.0",
             "source": {
                 "type": "git",
                 "url": "https://github.com/sebastianbergmann/global-state.git";,
-                "reference": "e8ba02eed7bbbb9e59e43dedd3dddeff4a56b0c4"
+                "reference": "edf8a461cf1d4005f19fb0b6b8b95a9f7fa0adc4"
             },
             "dist": {
                 "type": "zip",
-                "url": 
"https://api.github.com/repos/sebastianbergmann/global-state/zipball/e8ba02eed7bbbb9e59e43dedd3dddeff4a56b0c4";,
-                "reference": "e8ba02eed7bbbb9e59e43dedd3dddeff4a56b0c4",
+                "url": 
"https://api.github.com/repos/sebastianbergmann/global-state/zipball/edf8a461cf1d4005f19fb0b6b8b95a9f7fa0adc4";,
+                "reference": "edf8a461cf1d4005f19fb0b6b8b95a9f7fa0adc4",
                 "shasum": ""
             },
             "require": {
-                "php": "^7.0"
+                "php": "^7.2",
+                "sebastian/object-reflector": "^1.1.1",
+                "sebastian/recursion-context": "^3.0"
             },
             "require-dev": {
-                "phpunit/phpunit": "^6.0"
+                "ext-dom": "*",
+                "phpunit/phpunit": "^8.0"
             },
             "suggest": {
                 "ext-uopz": "*"
@@ -1075,7 +1076,7 @@
             "type": "library",
             "extra": {
                 "branch-alias": {
-                    "dev-master": "2.0-dev"
+                    "dev-master": "3.0-dev"
                 }
             },
             "autoload": {
@@ -1098,7 +1099,7 @@
             "keywords": [
                 "global state"
             ],
-            "time": "2017-04-27T15:39:26+00:00"
+            "time": "2019-02-01T05:30:01+00:00"
         },
         {
             "name": "sebastian/object-enumerator",
diff --git a/phpunit.xml.dist b/phpunit.xml.dist
index a793290..e19c661 100644
--- a/phpunit.xml.dist
+++ b/phpunit.xml.dist
@@ -6,7 +6,7 @@
         
xsi:noNamespaceSchemaLocation="https://schema.phpunit.de/7.4/phpunit.xsd";
         backupGlobals="false"
         colors="true"
-        bootstrap="vendor/autoload.php">
+        bootstrap="tests/bootstrap.php">
     <php>
         <ini name="error_reporting" value="-1" />
         <ini name="date.timezone" value="UTC" />
@@ -17,4 +17,10 @@
             <directory>tests/</directory>
         </testsuite>
     </testsuites>
+
+    <filter>
+        <whitelist>
+            <directory>./src</directory>
+        </whitelist>
+    </filter>
 </phpunit>
diff --git a/src/Autoloader.php b/src/Autoloader.php
index ae526e0..ecd1251 100644
--- a/src/Autoloader.php
+++ b/src/Autoloader.php
@@ -83,7 +83,7 @@ class Autoloader
         $prefix = trim($prefix, '\\') . '\\';
 
         // normalize the base directory with a trailing separator
-        $baseDir = rtrim($baseDir, DIRECTORY_SEPARATOR) . '/';
+        $baseDir = rtrim($baseDir, '\\/') . '/';
 
         // initialize the namespace prefix array
         if (isset($this->prefixes[$prefix]) === false) {
diff --git a/tests/AutoloaderTest.php b/tests/AutoloaderTest.php
deleted file mode 100644
index 16ef816..0000000
--- a/tests/AutoloaderTest.php
+++ /dev/null
@@ -1,99 +0,0 @@
-<?php
-
-namespace App\Tests;
-
-use App\Autoloader;
-use PHPUnit\Framework\TestCase;
-
-class MockAutoloader extends Autoloader
-{
-    protected $files = [];
-
-    public function setFiles(array $files)
-    {
-        $this->files = $files;
-    }
-
-    protected function requireFile($file)
-    {
-        return in_array($file, $this->files);
-    }
-}
-
-class AutoloaderTest extends TestCase
-{
-    protected $autoloader;
-
-    protected function setUp()
-    {
-        $this->autoloader = new MockAutoloader;
-
-        $this->autoloader->setFiles([
-            '/vendor/foo.bar/src/ClassName.php',
-            '/vendor/foo.bar/src/DoomClassName.php',
-            '/vendor/foo.bar/tests/ClassNameTest.php',
-            '/vendor/foo.bardoom/src/ClassName.php',
-            '/vendor/foo.bar.baz.dib/src/ClassName.php',
-            '/vendor/foo.bar.baz.dib.zim.gir/src/ClassName.php',
-            '/src/lib/ClassName.php',
-            '/src/libfoo/ClassFoo.php',
-        ]);
-
-        $this->autoloader->addNamespace(
-            'Foo\Bar',
-            '/vendor/foo.bar/src'
-        );
-
-        $this->autoloader->addNamespace(
-            'Foo\Bar',
-            '/vendor/foo.bar/tests'
-        );
-
-        $this->autoloader->addNamespace(
-            'Foo\\BarDoom',
-            '/vendor/foo.bardoom/src/'
-        );
-
-        $this->autoloader->addNamespace(
-            'Foo\Bar\Baz\Dib',
-            '/vendor/foo.bar.baz.dib/src/'
-        );
-
-        $this->autoloader->addNamespace(
-            'Foo\Bar\Baz\Dib\Zim\Gir',
-            '/vendor/foo.bar.baz.dib.zim.gir/src/'
-        );
-
-        $this->autoloader->addClassmap(
-            'ClassName',
-            '/src/lib/ClassName.php'
-        );
-
-        $this->autoloader->addClassmap(
-            'ClassFoo',
-            '/src/libfoo/ClassFoo.php'
-        );
-    }
-
-    /**
-     * @dataProvider classesProvider
-     */
-    public function testLoad($class, $expected)
-    {
-        $this->assertEquals($expected, $this->autoloader->load($class));
-    }
-
-    public function classesProvider()
-    {
-        return [
-            ['Foo\Bar\ClassName', '/vendor/foo.bar/src/ClassName.php'],
-            ['Foo\Bar\ClassNameTest', 
'/vendor/foo.bar/tests/ClassNameTest.php'],
-            ['ClassName', '/src/lib/ClassName.php'],
-            ['ClassFoo', '/src/libfoo/ClassFoo.php'],
-            ['No_Vendor\No_Package\NoClass', false],
-            ['Foo\Bar\Baz\Dib\Zim\Gir\ClassName', 
'/vendor/foo.bar.baz.dib.zim.gir/src/ClassName.php'],
-            ['Foo\Bar\DoomClassName', '/vendor/foo.bar/src/DoomClassName.php'],
-            ['Foo\BarDoom\ClassName', '/vendor/foo.bardoom/src/ClassName.php'],
-        ];
-    }
-}
diff --git a/tests/Container/ContainerTest.php 
b/tests/Container/ContainerTest.php
deleted file mode 100644
index 42e1a74..0000000
--- a/tests/Container/ContainerTest.php
+++ /dev/null
@@ -1,109 +0,0 @@
-<?php
-
-namespace App\Tests\Container;
-
-use App\Container\Container;
-use PHPUnit\Framework\TestCase;
-
-class ContainerTest extends TestCase
-{
-    public function testContainer()
-    {
-        // Create container
-        $container = new Container();
-
-        // Service definitions
-        $container->set(MockService::class, function ($c) {
-            $service = new MockService($c->get(MockDependency::class), 'foo');
-            $service->setProperty('group.param');
-
-            return $service;
-        });
-
-        $container->set(MockDependency::class, function ($c) {
-            return new MockDependency('group.param');
-        });
-
-        // Check retrieval of service
-        $service = $container->get(MockService::class);
-        $this->assertInstanceOf(MockService::class, $service);
-
-        // Check retrieval of dependency
-        $dependency = $container->get(MockDependency::class);
-        $this->assertInstanceOf(MockDependency::class, $dependency);
-
-        // Check that the dependency has been reused
-        $this->assertSame($dependency, $service->getDependency());
-
-        // Check the service calls have initialized
-        $this->assertEquals('group.param', $service->getProperty());
-    }
-
-    public function testHas()
-    {
-        $container = new Container();
-
-        $this->assertFalse($container->has(MockDependency::class));
-
-        $container->set(MockDependency::class, function ($c) {
-            return new MockDependency('group.param');
-        });
-
-        $this->assertFalse($container->has(MockService::class));
-        $this->assertTrue($container->has(MockDependency::class));
-    }
-
-    /**
-     * @expectedException App\Container\Exception\EntryNotFoundException
-     */
-    public function testServiceNotFound()
-    {
-        $container = new Container();
-        $container->get('foo');
-    }
-
-    /**
-     * @expectedException        App\Container\Exception\ContainerException
-     * @expectedExceptionMessage entry must be callable
-     */
-    public function testBadServiceEntry()
-    {
-        $container = new Container();
-        $container->set(\stdClass::class, '');
-        $container->get(\stdClass::class);
-    }
-
-    /**
-     * @expectedException        App\Container\Exception\ContainerException
-     * @expectedExceptionMessage circular reference
-     */
-    public function testCircularReference()
-    {
-        $container = new Container();
-
-        $container->set(MockService::class, function ($c) {
-            return new MockService($c->get(MockService::class));
-        });
-
-        $container->set(MockService::class, function ($c) {
-            return new MockService($c->get(MockService::class));
-        });
-
-        $container->get(MockService::class);
-    }
-
-    public function testParametersAndServices()
-    {
-        $container = new Container([
-            'foo' => 'bar',
-            'baz' => function ($c) {
-                return $c->get('foo');
-            },
-        ]);
-
-        $this->assertTrue($container->has('foo'));
-        $this->assertTrue($container->has('baz'));
-        $this->assertEquals('bar', $container->get('foo'));
-        $this->assertEquals('bar', $container->get('baz'));
-    }
-}
diff --git a/tests/Container/MockDependency.php 
b/tests/Container/MockDependency.php
deleted file mode 100644
index dc2c3a9..0000000
--- a/tests/Container/MockDependency.php
+++ /dev/null
@@ -1,21 +0,0 @@
-<?php
-
-namespace App\Tests\Container;
-
-/**
- * Mock service dependency class for testing container.
- */
-class MockDependency
-{
-    private $parameter;
-
-    public function __construct($parameter)
-    {
-        $this->parameter = $parameter;
-    }
-
-    public function getParameter()
-    {
-        return $this->parameter;
-    }
-}
diff --git a/tests/Container/MockService.php b/tests/Container/MockService.php
deleted file mode 100644
index 7d12d39..0000000
--- a/tests/Container/MockService.php
+++ /dev/null
@@ -1,32 +0,0 @@
-<?php
-
-namespace App\Tests\Container;
-
-/**
- * Mock service class for testing container.
- */
-class MockService
-{
-    private $dependency;
-    private $property;
-
-    public function __construct(MockDependency $dependency)
-    {
-        $this->dependency = $dependency;
-    }
-
-    public function getDependency()
-    {
-        return $this->dependency;
-    }
-
-    public function setProperty($value)
-    {
-        $this->property = $value;
-    }
-
-    public function getProperty()
-    {
-        return $this->property;
-    }
-}
diff --git a/tests/Mocks/responses/dev-body.txt 
b/tests/Mocks/responses/dev-body.txt
new file mode 100644
index 0000000..77697a6
--- /dev/null
+++ b/tests/Mocks/responses/dev-body.txt
@@ -0,0 +1 @@
+["5.6.40-dev","7.0.33-dev","7.1.25-dev","7.2.14-dev","7.2.14RC1","7.3.1-dev","7.3.1RC1"]
diff --git a/tests/Mocks/responses/stable-body.txt 
b/tests/Mocks/responses/stable-body.txt
new file mode 100644
index 0000000..d4daa41
--- /dev/null
+++ b/tests/Mocks/responses/stable-body.txt
@@ -0,0 +1 @@
+{"5":{"5.6":{"announcement":true,"source":[{"filename":"php-5.6.39.tar.bz2","name":"PHP
 5.6.39 
(tar.bz2)","sha256":"b3db2345f50c010b01fe041b4e0f66c5aa28eb325135136f153e18da01583ad5","date":"06
 Dec 2018"},{"filename":"php-5.6.39.tar.gz","name":"PHP 5.6.39 
(tar.gz)","sha256":"127b122b7d6c7f3c211c0ffa554979370c3131196137404a51a391d8e2e9c7bb","date":"06
 Dec 2018"},{"filename":"php-5.6.39.tar.xz","name":"PHP 5.6.39 
(tar.xz)","sha256":"8147576001a832ff3d03cb2980caa2d6b584a10624f87ac459fcd3948c6e4a10","date":"06
 Dec 
2018"}],"version":"5.6.39"}},"7":{"7.0":{"announcement":true,"source":[{"filename":"php-7.0.33.tar.bz2","name":"PHP
 7.0.33 
(tar.bz2)","sha256":"4933ea74298a1ba046b0246fe3771415c84dfb878396201b56cb5333abe86f07","date":"06
 Dec 2018"},{"filename":"php-7.0.33.tar.gz","name":"PHP 7.0.33 
(tar.gz)","sha256":"d71a6ecb6b13dc53fed7532a7f8f949c4044806f067502f8fb6f9facbb40452a","date":"06
 Dec 2018"},{"filename":"php-7.0.33.tar.xz","name":"PHP 7.0.33 
(tar.xz)","sha256":"ab8c5be6e32b1f8d032909dedaaaa4bbb1a209e519abb01a52ce3914f9a13d96","date":"06
 Dec 
2018"}],"version":"7.0.33"},"7.1":{"announcement":true,"source":[{"filename":"php-7.1.25.tar.bz2","name":"PHP
 7.1.25 
(tar.bz2)","sha256":"002cdc880ac7cfaede2c389204d366108847db0f3ac72edf1ba95c0577f9aaac","date":"06
 Dec 2018"},{"filename":"php-7.1.25.tar.gz","name":"PHP 7.1.25 
(tar.gz)","sha256":"7dc40e202140e8b4fb3d992c15a68d98dc06b805e6b218497d260abbe51f5958","date":"06
 Dec 2018"},{"filename":"php-7.1.25.tar.xz","name":"PHP 7.1.25 
(tar.xz)","sha256":"0fd8dad1903cd0b2d615a1fe4209f99e53b7292403c8ffa1919c0f4dd1eada88","date":"06
 Dec 
2018"}],"version":"7.1.25"},"7.2":{"announcement":true,"source":[{"filename":"php-7.2.13.tar.bz2","name":"PHP
 7.2.13 
(tar.bz2)","sha256":"5b4a46fb76491bcd3eee1213773382e570f6ecf9b22d623b24e2822298b3e92d","date":"06
 Dec 2018"},{"filename":"php-7.2.13.tar.gz","name":"PHP 7.2.13 
(tar.gz)","sha256":"e563cee406b1ec96649c22ed2b35796cfe4e9aa9afa6eab6be4cf2fe5d724744","date":"06
 Dec 2018"},{"filename":"php-7.2.13.tar.xz","name":"PHP 7.2.13 
(tar.xz)","sha256":"14b0429abdb46b65c843e5882c9a8c46b31dfbf279c747293b8ab950c2644a4b","date":"06
 Dec 
2018"}],"version":"7.2.13"},"7.3":{"announcement":true,"source":[{"filename":"php-7.3.0.tar.bz2","name":"PHP
 7.3.0 
(tar.bz2)","sha256":"7a267daec9969a997c5c8028c350229646748e0fcc71e2f2dbb157ddcee87c67","date":"06
 Dec 2018"},{"filename":"php-7.3.0.tar.gz","name":"PHP 7.3.0 
(tar.gz)","sha256":"391bd0f91d9bdd01ab47ef9607bad8c65e35bc9bb098fb7777b2556e2c847b11","date":"06
 Dec 2018"},{"filename":"php-7.3.0.tar.xz","name":"PHP 7.3.0 
(tar.xz)","sha256":"7d195cad55af8b288c3919c67023a14ff870a73e3acc2165a6d17a4850a560b5","date":"06
 Dec 2018"}],"version":"7.3.0"}}}
diff --git a/tests/Template/ContextTest.php b/tests/Template/ContextTest.php
deleted file mode 100644
index 5092f69..0000000
--- a/tests/Template/ContextTest.php
+++ /dev/null
@@ -1,78 +0,0 @@
-<?php
-
-namespace App\Tests\Template;
-
-use PHPUnit\Framework\TestCase;
-use App\Template\Context;
-
-class ContextTest extends TestCase
-{
-    public function setUp()
-    {
-        $this->context = new Context(__DIR__.'/../fixtures/templates');
-    }
-
-    public function testBlock()
-    {
-        $this->context->start('foo');
-        echo 'bar';
-        $this->context->end('foo');
-
-        $this->assertEquals($this->context->block('foo'), 'bar');
-
-        $this->context->append('foo');
-        echo 'baz';
-        $this->context->end('foo');
-
-        $this->assertEquals($this->context->block('foo'), 'barbaz');
-
-        $this->context->start('foo');
-        echo 'overridden';
-        $this->context->end('foo');
-
-        $this->assertEquals($this->context->block('foo'), 'overridden');
-    }
-
-    public function testInclude()
-    {
-        ob_start();
-        $this->context->include('includes/banner.php');
-        $content = ob_get_clean();
-
-        
$this->assertEquals(file_get_contents(__DIR__.'/../fixtures/templates/includes/banner.php'),
 $content);
-    }
-
-    public function testIncludeReturn()
-    {
-        $variable = $this->context->include('includes/variable.php');
-
-        $this->assertEquals(include 
__DIR__.'/../fixtures/templates/includes/variable.php', $variable);
-    }
-
-    /**
-     * @dataProvider attacksProvider
-     */
-    public function testEscaping($malicious, $escaped, $noHtml)
-    {
-        $this->assertEquals($escaped, $this->context->e($malicious));
-    }
-
-    /**
-     * @dataProvider attacksProvider
-     */
-    public function testNoHtml($malicious, $escaped, $noHtml)
-    {
-        $this->assertEquals($noHtml, $this->context->noHtml($malicious));
-    }
-
-    public function attacksProvider()
-    {
-        return [
-            [
-                '<iframe src="javascript:alert(\'Xss\')";></iframe>',
-                '&lt;iframe 
src=&quot;javascript:alert(&#039;Xss&#039;)&quot;;&gt;&lt;/iframe&gt;',
-                '&lt;iframe 
src&equals;&quot;javascript&colon;alert&lpar;&apos;Xss&apos;&rpar;&quot;&semi;&gt;&lt;&sol;iframe&gt;'
-            ]
-        ];
-    }
-}
diff --git a/tests/Template/EngineTest.php b/tests/Template/EngineTest.php
deleted file mode 100644
index 16174b5..0000000
--- a/tests/Template/EngineTest.php
+++ /dev/null
@@ -1,186 +0,0 @@
-<?php
-
-namespace App\Tests\Template;
-
-use PHPUnit\Framework\TestCase;
-use App\Template\Engine;
-
-class EngineTest extends TestCase
-{
-    public function setUp()
-    {
-        $this->template = new Engine(__DIR__.'/../fixtures/templates');
-    }
-
-    public function testView()
-    {
-        $content = $this->template->render('pages/view.php', [
-            'foo' => 'Lorem ipsum dolor sit amet.',
-            'sidebar' => 'PHP is a popular general-purpose scripting language 
that is especially suited to web development',
-        ]);
-
-        $this->assertRegexp('/Lorem ipsum dolor sit amet/', $content);
-        $this->assertRegexp('/PHP is a popular general-purpose/', $content);
-    }
-
-    public function testRegisterNew()
-    {
-        // Register callable function
-        $this->template->register('addAsterisks', function ($var) {
-            return '***'.$var.'***';
-        });
-
-        // Register callable object and method
-        $object = new class {
-            public $property;
-
-            public function doSomething($argument) {}
-        };
-        $this->template->register('doSomething', [$object, 'doSomething']);
-
-        $content = $this->template->render('pages/add_function.php', [
-            'foo' => 'Lorem ipsum dolor sit amet.',
-        ]);
-
-        $this->assertRegexp('/\*\*\*Lorem ipsum dolor sit amet\.\*\*\*/', 
$content);
-    }
-
-    public function testRegisterExisting()
-    {
-        $this->expectException(\Exception::class);
-
-        $this->template->register('noHtml', function ($var) {
-            return $var;
-        });
-    }
-
-    public function testAssignments()
-    {
-        $this->template->assign([
-            'parameter' => 'FooBarBaz',
-        ]);
-
-        $content = $this->template->render('pages/assignments.php', [
-            'foo' => 'Lorem ipsum dolor sit amet.',
-        ]);
-
-        $this->assertRegexp('/Lorem ipsum dolor sit amet\./', $content);
-        $this->assertRegexp('/FooBarBaz/', $content);
-    }
-
-    public function testMerge()
-    {
-        $this->template->assign([
-            'foo',
-            'bar',
-            'qux' => 'quux',
-        ]);
-
-        $this->template->assign([
-            'baz',
-            'qux' => 'quuz',
-        ]);
-
-        $this->assertEquals(['baz', 'bar', 'qux' => 'quuz'], 
$this->template->getVariables());
-    }
-
-    public function testVariablesScope()
-    {
-        $this->template->assign([
-            'parameter' => 'Parameter value',
-        ]);
-
-        $content = $this->template->render('pages/invalid_variables.php', [
-            'foo' => 'Lorem ipsum dolor sit amet',
-        ]);
-
-        $expected = var_export([
-            'parameter' => 'Parameter value',
-            'foo' => 'Lorem ipsum dolor sit amet',
-        ], true);
-
-        $this->assertEquals($expected, $content);
-    }
-
-    public function testInvalidVariables()
-    {
-        $this->template->assign([
-            'Invalid value with key 0',
-            'parameter' => 'Parameter value',
-            'Invalid value with key 1',
-        ]);
-
-        $this->expectException(\Exception::class);
-
-        $content = $this->template->render('pages/invalid_variables.php', [
-            'foo' => 'Lorem ipsum dolor sit amet',
-            1 => 'Invalid overridden value with key 1',
-        ]);
-    }
-
-    public function testOverrides()
-    {
-        $this->template->assign([
-            'pageParameter_1' => 'Page parameter 1',
-            'pageParameter_2' => 'Page parameter 2',
-            'layoutParameter_1' => 'Layout parameter 1',
-            'layoutParameter_2' => 'Layout parameter 2',
-            'layoutParameter_3' => 'Layout parameter 3',
-        ]);
-
-        $content = $this->template->render('pages/overrides.php', [
-            'pageParameter_2' => 'Overridden parameter 2',
-            'layoutParameter_2' => 'Layout overridden parameter 2',
-        ]);
-
-        $this->assertRegexp('/Page parameter 1/', $content);
-        $this->assertRegexp('/^((?!Page parameter 2).)*$/s', $content);
-        $this->assertRegexp('/Overridden parameter 2/', $content);
-        $this->assertRegexp('/Layout parameter 1/', $content);
-        $this->assertRegexp('/^((?!Layout parameter 2).)*$/s', $content);
-        $this->assertRegexp('/Layout overridden parameter 2/', $content);
-    }
-
-    public function testAppending()
-    {
-        $content = $this->template->render('pages/appending.php');
-
-        $this->assertRegexp('/file\_1\.js/', $content);
-        $this->assertRegexp('/file\_2\.js/', $content);
-    }
-
-    public function testIncluding()
-    {
-        $content = $this->template->render('pages/including.php');
-
-        $this->assertRegexp('/\<form method\=\"post\"\>/', $content);
-        $this->assertRegexp('/Banner inclusion/', $content);
-    }
-
-    public function testNoLayout()
-    {
-        $content = $this->template->render('pages/no_layout.rss');
-
-        
$this->assertEquals(file_get_contents(__DIR__.'/../fixtures/templates/pages/no_layout.rss'),
 $content);
-    }
-
-    public function testMissingTemplate()
-    {
-        $this->template->assign([
-            'parameter' => 'Parameter value',
-        ]);
-
-        $this->expectException(\Exception::class);
-
-        $content = $this->template->render('pages/this/does/not/exist.php', [
-            'foo' => 'Lorem ipsum dolor sit amet',
-        ]);
-    }
-
-    public function testExtending()
-    {
-        $this->expectException(\Exception::class);
-
-        $html = $this->template->render('pages/extends.php');
-    }
-}
diff --git a/tests/Unit/AutoloaderTest.php b/tests/Unit/AutoloaderTest.php
new file mode 100644
index 0000000..164c158
--- /dev/null
+++ b/tests/Unit/AutoloaderTest.php
@@ -0,0 +1,100 @@
+<?php declare(strict_types=1);
+
+namespace App\Tests\Unit;
+
+use App\Autoloader;
+use PHPUnit\Framework\TestCase;
+
+class MockAutoloader extends Autoloader
+{
+    protected $files = [];
+
+    public function setFiles(array $files)
+    {
+        $this->files = $files;
+    }
+
+    protected function requireFile($file)
+    {
+        return in_array($file, $this->files);
+    }
+}
+
+class AutoloaderTest extends TestCase
+{
+    /** @var MockAutoloader */
+    protected $autoloader;
+
+    protected function setUp(): void
+    {
+        $this->autoloader = new MockAutoloader;
+
+        $this->autoloader->setFiles([
+            '/vendor/foo.bar/src/ClassName.php',
+            '/vendor/foo.bar/src/DoomClassName.php',
+            '/vendor/foo.bar/tests/ClassNameTest.php',
+            '/vendor/foo.bardoom/src/ClassName.php',
+            '/vendor/foo.bar.baz.dib/src/ClassName.php',
+            '/vendor/foo.bar.baz.dib.zim.gir/src/ClassName.php',
+            '/src/lib/ClassName.php',
+            '/src/libfoo/ClassFoo.php',
+        ]);
+
+        $this->autoloader->addNamespace(
+            'Foo\Bar',
+            '/vendor/foo.bar/src'
+        );
+
+        $this->autoloader->addNamespace(
+            'Foo\Bar',
+            '/vendor/foo.bar/tests'
+        );
+
+        $this->autoloader->addNamespace(
+            'Foo\\BarDoom',
+            '/vendor/foo.bardoom/src/'
+        );
+
+        $this->autoloader->addNamespace(
+            'Foo\Bar\Baz\Dib',
+            '/vendor/foo.bar.baz.dib/src/'
+        );
+
+        $this->autoloader->addNamespace(
+            'Foo\Bar\Baz\Dib\Zim\Gir',
+            '/vendor/foo.bar.baz.dib.zim.gir/src/'
+        );
+
+        $this->autoloader->addClassmap(
+            'ClassName',
+            '/src/lib/ClassName.php'
+        );
+
+        $this->autoloader->addClassmap(
+            'ClassFoo',
+            '/src/libfoo/ClassFoo.php'
+        );
+    }
+
+    /**
+     * @dataProvider classesProvider
+     */
+    public function testLoad(string $class, $expected): void
+    {
+        $this->assertEquals($expected, $this->autoloader->load($class));
+    }
+
+    public function classesProvider(): array
+    {
+        return [
+            ['Foo\Bar\ClassName', '/vendor/foo.bar/src/ClassName.php'],
+            ['Foo\Bar\ClassNameTest', 
'/vendor/foo.bar/tests/ClassNameTest.php'],
+            ['ClassName', '/src/lib/ClassName.php'],
+            ['ClassFoo', '/src/libfoo/ClassFoo.php'],
+            ['No_Vendor\No_Package\NoClass', false],
+            ['Foo\Bar\Baz\Dib\Zim\Gir\ClassName', 
'/vendor/foo.bar.baz.dib.zim.gir/src/ClassName.php'],
+            ['Foo\Bar\DoomClassName', '/vendor/foo.bar/src/DoomClassName.php'],
+            ['Foo\BarDoom\ClassName', '/vendor/foo.bardoom/src/ClassName.php'],
+        ];
+    }
+}
diff --git a/tests/Unit/Container/ContainerTest.php 
b/tests/Unit/Container/ContainerTest.php
new file mode 100644
index 0000000..9d33cb5
--- /dev/null
+++ b/tests/Unit/Container/ContainerTest.php
@@ -0,0 +1,110 @@
+<?php declare(strict_types=1);
+
+namespace App\Tests\Unit\Container;
+
+use App\Container\Container;
+use App\Container\Exception\ContainerException;
+use App\Container\Exception\EntryNotFoundException;
+use PHPUnit\Framework\TestCase;
+
+class ContainerTest extends TestCase
+{
+    public function testContainer(): void
+    {
+        // Create container
+        $container = new Container();
+
+        // Service definitions
+        $container->set(MockService::class, function (Container $container) {
+            $service = new MockService($container->get(MockDependency::class));
+            $service->setProperty('group.param');
+
+            return $service;
+        });
+
+        $container->set(MockDependency::class, function (Container $container) 
{
+            return new MockDependency('group.param');
+        });
+
+        // Check retrieval of service
+        $service = $container->get(MockService::class);
+        $this->assertInstanceOf(MockService::class, $service);
+
+        // Check retrieval of dependency
+        $dependency = $container->get(MockDependency::class);
+        $this->assertInstanceOf(MockDependency::class, $dependency);
+
+        // Check that the dependency has been reused
+        $this->assertSame($dependency, $service->getDependency());
+
+        // Check the service calls have initialized
+        $this->assertEquals('group.param', $service->getProperty());
+    }
+
+    public function testHas(): void
+    {
+        $container = new Container();
+
+        $this->assertFalse($container->has(MockDependency::class));
+
+        $container->set(MockDependency::class, function (Container $container) 
{
+            return new MockDependency('group.param');
+        });
+
+        $this->assertFalse($container->has(MockService::class));
+        $this->assertTrue($container->has(MockDependency::class));
+    }
+
+    public function testServiceNotFound(): void
+    {
+        $container = new Container();
+
+        $this->expectException(EntryNotFoundException::class);
+
+        $container->get('foo');
+    }
+
+    public function testBadServiceEntry(): void
+    {
+        $container = new Container();
+        $container->set(\stdClass::class, '');
+
+        $this->expectException(ContainerException::class);
+        $this->expectExceptionMessage('entry must be callable');
+
+        $container->get(\stdClass::class);
+    }
+
+    public function testCircularReference(): void
+    {
+        $container = new Container();
+
+        $container->set(MockService::class, function (Container $container) {
+            return new MockService($container->get(MockService::class));
+        });
+
+        $container->set(MockService::class, function (Container $container) {
+            return new MockService($container->get(MockService::class));
+        });
+
+        $this->expectException(ContainerException::class);
+        $this->expectExceptionMessage('circular reference');
+
+        $container->get(MockService::class);
+    }
+
+    public function testParametersAndServices(): void
+    {
+        $container = new Container([
+            'foo' => 'bar',
+            'baz' => function (Container $container) {
+                return $container->get('foo');
+            },
+        ]);
+
+        $this->assertTrue($container->has('foo'));
+        $this->assertTrue($container->has('baz'));
+        $this->assertEquals('bar', $container->get('foo'));
+        $this->assertEquals('bar', $container->get('baz'));
+    }
+}
diff --git a/tests/Unit/Container/MockDependency.php 
b/tests/Unit/Container/MockDependency.php
new file mode 100644
index 0000000..fe78b99
--- /dev/null
+++ b/tests/Unit/Container/MockDependency.php
@@ -0,0 +1,21 @@
+<?php declare(strict_types=1);
+
+namespace App\Tests\Unit\Container;
+
+/**
+ * Mock service dependency class for testing container.
+ */
+class MockDependency
+{
+    private $parameter;
+
+    public function __construct(string $parameter)
+    {
+        $this->parameter = $parameter;
+    }
+
+    public function getParameter(): string
+    {
+        return $this->parameter;
+    }
+}
diff --git a/tests/Unit/Container/MockService.php 
b/tests/Unit/Container/MockService.php
new file mode 100644
index 0000000..a410bac
--- /dev/null
+++ b/tests/Unit/Container/MockService.php
@@ -0,0 +1,32 @@
+<?php declare(strict_types=1);
+
+namespace App\Tests\Unit\Container;
+
+/**
+ * Mock service class for testing container.
+ */
+class MockService
+{
+    private $dependency;
+    private $property;
+
+    public function __construct(MockDependency $dependency)
+    {
+        $this->dependency = $dependency;
+    }
+
+    public function getDependency(): MockDependency
+    {
+        return $this->dependency;
+    }
+
+    public function setProperty(string $value): void
+    {
+        $this->property = $value;
+    }
+
+    public function getProperty(): string
+    {
+        return $this->property;
+    }
+}
diff --git a/tests/Unit/Template/ContextTest.php 
b/tests/Unit/Template/ContextTest.php
new file mode 100644
index 0000000..8b82de7
--- /dev/null
+++ b/tests/Unit/Template/ContextTest.php
@@ -0,0 +1,81 @@
+<?php declare(strict_types=1);
+
+namespace App\Tests\Unit\Template;
+
+use PHPUnit\Framework\TestCase;
+use App\Template\Context;
+
+class ContextTest extends TestCase
+{
+    /** @var Context */
+    private $context;
+
+    public function setUp(): void
+    {
+        $this->context = new Context(TEST_FIXTURES_DIRECTORY . '/templates');
+    }
+
+    public function testBlock(): void
+    {
+        $this->context->start('foo');
+        echo 'bar';
+        $this->context->end('foo');
+
+        $this->assertEquals($this->context->block('foo'), 'bar');
+
+        $this->context->append('foo');
+        echo 'baz';
+        $this->context->end('foo');
+
+        $this->assertEquals($this->context->block('foo'), 'barbaz');
+
+        $this->context->start('foo');
+        echo 'overridden';
+        $this->context->end('foo');
+
+        $this->assertEquals($this->context->block('foo'), 'overridden');
+    }
+
+    public function testInclude(): void
+    {
+        ob_start();
+        $this->context->include('includes/banner.php');
+        $content = ob_get_clean();
+
+        $this->assertEquals(file_get_contents(TEST_FIXTURES_DIRECTORY . 
'/templates/includes/banner.php'), $content);
+    }
+
+    public function testIncludeReturn(): void
+    {
+        $variable = $this->context->include('includes/variable.php');
+
+        $this->assertEquals(include TEST_FIXTURES_DIRECTORY . 
'/templates/includes/variable.php', $variable);
+    }
+
+    /**
+     * @dataProvider attacksProvider
+     */
+    public function testEscaping(string $malicious, string $escaped, string 
$noHtml): void
+    {
+        $this->assertEquals($escaped, $this->context->e($malicious));
+    }
+
+    /**
+     * @dataProvider attacksProvider
+     */
+    public function testNoHtml(string $malicious, string $escaped, string 
$noHtml): void
+    {
+        $this->assertEquals($noHtml, $this->context->noHtml($malicious));
+    }
+
+    public function attacksProvider(): array
+    {
+        return [
+            [
+                '<iframe src="javascript:alert(\'Xss\')";></iframe>',
+                '&lt;iframe 
src=&quot;javascript:alert(&#039;Xss&#039;)&quot;;&gt;&lt;/iframe&gt;',
+                '&lt;iframe 
src&equals;&quot;javascript&colon;alert&lpar;&apos;Xss&apos;&rpar;&quot;&semi;&gt;&lt;&sol;iframe&gt;'
+            ]
+        ];
+    }
+}
diff --git a/tests/Unit/Template/EngineTest.php 
b/tests/Unit/Template/EngineTest.php
new file mode 100644
index 0000000..30f8337
--- /dev/null
+++ b/tests/Unit/Template/EngineTest.php
@@ -0,0 +1,189 @@
+<?php declare(strict_types=1);
+
+namespace App\Tests\Unit\Template;
+
+use PHPUnit\Framework\TestCase;
+use App\Template\Engine;
+
+class EngineTest extends TestCase
+{
+    /** @var Engine */
+    private $template;
+
+    public function setUp(): void
+    {
+        $this->template = new Engine(TEST_FIXTURES_DIRECTORY . '/templates');
+    }
+
+    public function testView(): void
+    {
+        $content = $this->template->render('pages/view.php', [
+            'foo'     => 'Lorem ipsum dolor sit amet.',
+            'sidebar' => 'PHP is a popular general-purpose scripting language 
that is especially suited to web development',
+        ]);
+
+        $this->assertRegexp('/Lorem ipsum dolor sit amet/', $content);
+        $this->assertRegexp('/PHP is a popular general-purpose/', $content);
+    }
+
+    public function testRegisterNew(): void
+    {
+        // Register callable function
+        $this->template->register('addAsterisks', function ($var) {
+            return '***'.$var.'***';
+        });
+
+        // Register callable object and method
+        $object = new class {
+            public $property;
+
+            public function doSomething($argument) {}
+        };
+        $this->template->register('doSomething', [$object, 'doSomething']);
+
+        $content = $this->template->render('pages/add_function.php', [
+            'foo' => 'Lorem ipsum dolor sit amet.',
+        ]);
+
+        $this->assertRegexp('/\*\*\*Lorem ipsum dolor sit amet\.\*\*\*/', 
$content);
+    }
+
+    public function testRegisterExisting(): void
+    {
+        $this->expectException(\Exception::class);
+
+        $this->template->register('noHtml', function ($var) {
+            return $var;
+        });
+    }
+
+    public function testAssignments(): void
+    {
+        $this->template->assign([
+            'parameter' => 'FooBarBaz',
+        ]);
+
+        $content = $this->template->render('pages/assignments.php', [
+            'foo' => 'Lorem ipsum dolor sit amet.',
+        ]);
+
+        $this->assertRegexp('/Lorem ipsum dolor sit amet\./', $content);
+        $this->assertRegexp('/FooBarBaz/', $content);
+    }
+
+    public function testMerge(): void
+    {
+        $this->template->assign([
+            'foo',
+            'bar',
+            'qux' => 'quux',
+        ]);
+
+        $this->template->assign([
+            'baz',
+            'qux' => 'quuz',
+        ]);
+
+        $this->assertEquals(['baz', 'bar', 'qux' => 'quuz'], 
$this->template->getVariables());
+    }
+
+    public function testVariablesScope(): void
+    {
+        $this->template->assign([
+            'parameter' => 'Parameter value',
+        ]);
+
+        $content = $this->template->render('pages/invalid_variables.php', [
+            'foo' => 'Lorem ipsum dolor sit amet',
+        ]);
+
+        $expected = var_export([
+            'parameter' => 'Parameter value',
+            'foo'       => 'Lorem ipsum dolor sit amet',
+        ], true);
+
+        $this->assertEquals($expected, $content);
+    }
+
+    public function testInvalidVariables(): void
+    {
+        $this->template->assign([
+            'Invalid value with key 0',
+            'parameter' => 'Parameter value',
+            'Invalid value with key 1',
+        ]);
+
+        $this->expectException(\Exception::class);
+
+        $this->template->render('pages/invalid_variables.php', [
+            'foo' => 'Lorem ipsum dolor sit amet',
+            1     => 'Invalid overridden value with key 1',
+        ]);
+    }
+
+    public function testOverrides(): void
+    {
+        $this->template->assign([
+            'pageParameter_1'   => 'Page parameter 1',
+            'pageParameter_2'   => 'Page parameter 2',
+            'layoutParameter_1' => 'Layout parameter 1',
+            'layoutParameter_2' => 'Layout parameter 2',
+            'layoutParameter_3' => 'Layout parameter 3',
+        ]);
+
+        $content = $this->template->render('pages/overrides.php', [
+            'pageParameter_2'   => 'Overridden parameter 2',
+            'layoutParameter_2' => 'Layout overridden parameter 2',
+        ]);
+
+        $this->assertRegexp('/Page parameter 1/', $content);
+        $this->assertRegexp('/^((?!Page parameter 2).)*$/s', $content);
+        $this->assertRegexp('/Overridden parameter 2/', $content);
+        $this->assertRegexp('/Layout parameter 1/', $content);
+        $this->assertRegexp('/^((?!Layout parameter 2).)*$/s', $content);
+        $this->assertRegexp('/Layout overridden parameter 2/', $content);
+    }
+
+    public function testAppending(): void
+    {
+        $content = $this->template->render('pages/appending.php');
+
+        $this->assertRegexp('/file\_1\.js/', $content);
+        $this->assertRegexp('/file\_2\.js/', $content);
+    }
+
+    public function testIncluding(): void
+    {
+        $content = $this->template->render('pages/including.php');
+
+        $this->assertRegexp('/\<form method\=\"post\"\>/', $content);
+        $this->assertRegexp('/Banner inclusion/', $content);
+    }
+
+    public function testNoLayout(): void
+    {
+        $content = $this->template->render('pages/no_layout.rss');
+
+        $this->assertEquals(file_get_contents(TEST_FIXTURES_DIRECTORY . 
'/templates/pages/no_layout.rss'), $content);
+    }
+
+    public function testMissingTemplate(): void
+    {
+        $this->template->assign([
+            'parameter' => 'Parameter value',
+        ]);
+
+        $this->expectException(\Exception::class);
+
+        $this->template->render('pages/this/does/not/exist.php', [
+            'foo' => 'Lorem ipsum dolor sit amet',
+        ]);
+    }
+
+    public function testExtending(): void
+    {
+        $this->expectException(\Exception::class);
+
+        $this->template->render('pages/extends.php');
+    }
+}
diff --git a/tests/Unit/Utils/CacheTest.php b/tests/Unit/Utils/CacheTest.php
new file mode 100644
index 0000000..ab5dbc2
--- /dev/null
+++ b/tests/Unit/Utils/CacheTest.php
@@ -0,0 +1,44 @@
+<?php declare(strict_types=1);
+
+namespace App\Tests\Unit\Utils;
+
+use App\Utils\Cache;
+use PHPUnit\Framework\TestCase;
+
+class CacheTest extends TestCase
+{
+    /** @var string */
+    private $cacheDir = TEST_VAR_DIRECTORY . '/cache/test';
+
+    /** @var Cache */
+    private $cache;
+
+    public function setUp(): void
+    {
+        $this->cache = new Cache($this->cacheDir);
+        $this->cache->clear();
+    }
+
+    public function tearDown(): void
+    {
+        $this->cache->clear();
+        rmdir($this->cacheDir);
+    }
+
+    public function testHas(): void
+    {
+        $this->assertFalse($this->cache->has('foo'));
+
+        $this->cache->set('foo', [1, 2, 3]);
+        $this->assertTrue($this->cache->has('foo'));
+    }
+
+    public function testDelete(): void
+    {
+        $this->cache->set('bar', [1, 2, 3]);
+        $this->assertFileExists($this->cacheDir.'/bar.php');
+
+        $this->cache->delete('bar');
+        $this->assertFalse(file_exists($this->cacheDir.'/bar.php'));
+    }
+}
diff --git a/tests/Unit/Utils/CaptchaTest.php b/tests/Unit/Utils/CaptchaTest.php
new file mode 100644
index 0000000..acc41af
--- /dev/null
+++ b/tests/Unit/Utils/CaptchaTest.php
@@ -0,0 +1,42 @@
+<?php declare(strict_types=1);
+
+namespace App\Tests\Unit\Utils;
+
+use PHPUnit\Framework\TestCase;
+use App\Utils\Captcha;
+
+class CaptchaTest extends TestCase
+{
+    /** @var Captcha */
+    private $captcha;
+
+    public function setUp(): void
+    {
+        $this->captcha = new Captcha();
+    }
+
+    /**
+     * @dataProvider mathProvider
+     */
+    public function testGetQuestion(int $first, int $last, string $operation, 
string $question, int $expected): void
+    {
+        $this->captcha->setFirst($first);
+        $this->captcha->setLast($last);
+        $this->captcha->setOperation($operation);
+
+        $this->assertEquals($question, $this->captcha->getQuestion());
+        $this->assertEquals($expected, $this->captcha->getAnswer());
+    }
+
+    public function mathProvider(): array
+    {
+        return [
+            [1, 2, 'addition', '1 + 2 = ?', 3],
+            [10, 50, 'subtraction', '50 - 10 = ?', 40],
+            [90, 50, 'subtraction', '90 - 50 = ?', 40],
+            [14, 14, 'subtraction', '14 - 14 = ?', 0],
+            [10, 5, 'multiplication', '10 + 5 = ?', 15],
+            [12, 2, 'foo', '12 + 2 = ?', 14],
+        ];
+    }
+}
diff --git a/tests/Unit/Utils/UploaderTest.php 
b/tests/Unit/Utils/UploaderTest.php
new file mode 100644
index 0000000..07bd684
--- /dev/null
+++ b/tests/Unit/Utils/UploaderTest.php
@@ -0,0 +1,56 @@
+<?php declare(strict_types=1);
+
+namespace App\Tests\Unit\Utils;
+
+use PHPUnit\Framework\MockObject\MockObject;
+use PHPUnit\Framework\TestCase;
+use App\Utils\Uploader;
+
+class UploaderTest extends TestCase
+{
+    /** @var string */
+    private $fixturesDirectory = TEST_FIXTURES_DIRECTORY . '/files';
+
+    /**
+     * @dataProvider filesProvider
+     */
+    public function testUpload(string $validExtension, array $file): void
+    {
+        $_FILES = ['uploaded' => $file];
+
+        /** @var Uploader|MockObject $uploader */
+        $uploader = $this->getMockBuilder(Uploader::class)
+            ->setMethods(['isUploadedFile', 'moveUploadedFile'])
+            ->getMock();
+
+        $uploader->expects($this->once())
+                 ->method('isUploadedFile')
+                 ->will($this->returnValue(true));
+
+        $uploader->expects($this->once())
+                 ->method('moveUploadedFile')
+                 ->will($this->returnValue(true));
+
+        $uploader->setMaxFileSize(100 * 1024);
+        $uploader->setValidExtension($validExtension);
+        $uploader->setDir(TEST_VAR_DIRECTORY . '/uploads');
+        $tmpFile = $uploader->upload('uploaded');
+
+        $this->assertNotNull($tmpFile);
+    }
+
+    public function filesProvider(): array
+    {
+        return [
+            [
+                'txt',
+                [
+                    'name'     => 'patch.txt',
+                    'tmp_name' => $this->fixturesDirectory . '/patch.txt',
+                    'size'     => filesize($this->fixturesDirectory . 
'/patch.txt'),
+                    'error'    => UPLOAD_ERR_OK,
+                ]
+            ],
+        ];
+    }
+}
diff --git a/tests/Unit/Utils/Versions/ClientTest.php 
b/tests/Unit/Utils/Versions/ClientTest.php
new file mode 100644
index 0000000..3577939
--- /dev/null
+++ b/tests/Unit/Utils/Versions/ClientTest.php
@@ -0,0 +1,37 @@
+<?php declare(strict_types=1);
+
+namespace App\Tests\Unit\Utils\Versions;
+
+use PHPUnit\Framework\TestCase;
+use App\Utils\Versions\Client;
+
+class ClientTest extends TestCase
+{
+    /** @var Client */
+    private $client;
+
+    public function setUp(): void
+    {
+        $this->client = new Client();
+
+        $reflection = new \ReflectionClass($this->client);
+
+        $devVersionsUrl = $reflection->getProperty('devVersionsUrl');
+        $devVersionsUrl->setAccessible(true);
+        $devVersionsUrl->setValue($this->client, TEST_MOCKS_DIRECTORY . 
'/responses/dev-body.txt');
+
+        $stableVersionsUrl = $reflection->getProperty('stableVersionsUrl');
+        $stableVersionsUrl->setAccessible(true);
+        $stableVersionsUrl->setValue($this->client, TEST_MOCKS_DIRECTORY . 
'/responses/stable-body.txt');
+    }
+
+    public function testFetchDevVersions(): void
+    {
+        $this->assertIsArray($this->client->fetchDevVersions());
+    }
+
+    public function testFetchStableVersions(): void
+    {
+        $this->assertIsArray($this->client->fetchStableVersions());
+    }
+}
diff --git a/tests/Unit/Utils/Versions/GeneratorTest.php 
b/tests/Unit/Utils/Versions/GeneratorTest.php
new file mode 100644
index 0000000..d2e28a0
--- /dev/null
+++ b/tests/Unit/Utils/Versions/GeneratorTest.php
@@ -0,0 +1,77 @@
+<?php declare(strict_types=1);
+
+namespace App\Tests\Unit\Utils\Versions;
+
+use PHPUnit\Framework\TestCase;
+use App\Utils\Versions\Generator;
+use App\Utils\Versions\Client;
+use App\Utils\Cache;
+
+class GeneratorTest extends TestCase
+{
+    /** @var string */
+    private $cacheDir = TEST_VAR_DIRECTORY . '/cache/test';
+
+    /** @var Cache */
+    private $cache;
+
+    /** @var Client */
+    private $client;
+
+    /** @var Generator */
+    private $generator;
+
+    public function setUp(): void
+    {
+        $this->cache = new Cache($this->cacheDir);
+        $this->cache->clear();
+
+        // The results returned by the client depend on the remote URLs so we
+        // mock the returned results.
+        $this->client = $this->getMockBuilder(Client::class)
+            ->setMethods(['fetchDevVersions', 'fetchStableVersions'])
+            ->getMock();
+
+        $this->client->expects($this->once())
+            ->method('fetchDevVersions')
+            
->will($this->returnValue(json_decode(file_get_contents(TEST_MOCKS_DIRECTORY . 
'/responses/dev-body.txt', true))));
+
+        $this->client->expects($this->once())
+            ->method('fetchStableVersions')
+            
->will($this->returnValue(json_decode(file_get_contents(TEST_MOCKS_DIRECTORY . 
'/responses/stable-body.txt'), true)));
+
+        $this->generator = $this->getMockBuilder(Generator::class)
+            ->setConstructorArgs([$this->client, $this->cache])
+            ->setMethods(['getAffixes'])
+            ->getMock();
+
+        // The extra versions are always date dependant so we mock it to 
include
+        // static date done on the tests day.
+        $date = '2018-12-26';
+        $this->generator->expects($this->any())
+            ->method('getAffixes')
+            ->will($this->returnValue(['Git-' . $date . ' (snap)', 'Git-' . 
$date . ' (Git)',]));
+    }
+
+    public function tearDown(): void
+    {
+        $this->cache->clear();
+        rmdir($this->cacheDir);
+    }
+
+    public function testVersions(): void
+    {
+        $versions = $this->generator->getVersions();
+
+        $this->assertIsArray($versions);
+        $this->assertGreaterThan(5, count($versions));
+
+        $fixture = require TEST_FIXTURES_DIRECTORY . '/versions/versions.php';
+        $cached = require $this->cacheDir . '/versions.php';
+
+        $this->assertEquals($fixture[1], $cached[1]);
+        $this->assertContains('Next Major Version', $versions);
+        $this->assertContains('Irrelevant', $versions);
+        $this->assertContains('7.2.14RC1', $versions);
+    }
+}
diff --git a/tests/Utils/CacheTest.php b/tests/Utils/CacheTest.php
deleted file mode 100644
index 0e045ed..0000000
--- a/tests/Utils/CacheTest.php
+++ /dev/null
@@ -1,41 +0,0 @@
-<?php
-
-namespace App\Tests\Utils;
-
-use App\Utils\Cache;
-use PHPUnit\Framework\TestCase;
-
-class CacheTest extends TestCase
-{
-    private $cacheDir = __DIR__.'/../../var/cache/test';
-    private $cache;
-
-    public function setUp()
-    {
-        $this->cache = new Cache($this->cacheDir);
-        $this->cache->clear();
-    }
-
-    public function tearDown()
-    {
-        $this->cache->clear();
-        rmdir($this->cacheDir);
-    }
-
-    public function testHas()
-    {
-        $this->assertFalse($this->cache->has('foo'));
-
-        $this->cache->set('foo', [1, 2, 3]);
-        $this->assertTrue($this->cache->has('foo'));
-    }
-
-    public function testDelete()
-    {
-        $this->cache->set('bar', [1, 2, 3]);
-        $this->assertFileExists($this->cacheDir.'/bar.php');
-
-        $this->cache->delete('bar');
-        $this->assertFalse(file_exists($this->cacheDir.'/bar.php'));
-    }
-}
diff --git a/tests/Utils/CaptchaTest.php b/tests/Utils/CaptchaTest.php
deleted file mode 100644
index 681da7a..0000000
--- a/tests/Utils/CaptchaTest.php
+++ /dev/null
@@ -1,41 +0,0 @@
-<?php
-
-namespace App\Tests\Utils;
-
-use PHPUnit\Framework\TestCase;
-use App\Utils\Captcha;
-
-class CaptchaTest extends TestCase
-{
-    private $captcha;
-
-    public function setUp()
-    {
-        $this->captcha = new Captcha();
-    }
-
-    /**
-     * @dataProvider mathProvider
-     */
-    public function testGetQuestion($first, $last, $operation, $question, 
$expected)
-    {
-        $this->captcha->setFirst($first);
-        $this->captcha->setLast($last);
-        $this->captcha->setOperation($operation);
-
-        $this->assertEquals($question, $this->captcha->getQuestion());
-        $this->assertEquals($expected, $this->captcha->getAnswer());
-    }
-
-    public function mathProvider()
-    {
-        return [
-            [1, 2, 'addition', '1 + 2 = ?', 3],
-            [10, 50, 'subtraction', '50 - 10 = ?', 40],
-            [90, 50, 'subtraction', '90 - 50 = ?', 40],
-            [14, 14, 'subtraction', '14 - 14 = ?', 0],
-            [10, 5, 'multiplication', '10 + 5 = ?', 15],
-            [12, 2, 'foo', '12 + 2 = ?', 14],
-        ];
-    }
-}
diff --git a/tests/Utils/UploaderTest.php b/tests/Utils/UploaderTest.php
deleted file mode 100644
index 982cc7f..0000000
--- a/tests/Utils/UploaderTest.php
+++ /dev/null
@@ -1,54 +0,0 @@
-<?php
-
-namespace App\Tests\Utils;
-
-use PHPUnit\Framework\TestCase;
-use App\Utils\Uploader;
-
-class UploaderTest extends TestCase
-{
-    private $fixturesDirectory = __DIR__.'/../fixtures/files';
-
-    /**
-     * @dataProvider filesProvider
-     */
-    public function testUpload($validExtension, $file)
-    {
-        $_FILES = [];
-        $_FILES['uploaded'] = $file;
-
-        $uploader = $this->getMockBuilder(Uploader::class)
-            ->setMethods(['isUploadedFile', 'moveUploadedFile'])
-            ->getMock();
-
-        $uploader->expects($this->once())
-                 ->method('isUploadedFile')
-                 ->will($this->returnValue(true));
-
-        $uploader->expects($this->once())
-                 ->method('moveUploadedFile')
-                 ->will($this->returnValue(true));
-
-        $uploader->setMaxFileSize(100 * 1024);
-        $uploader->setValidExtension($validExtension);
-        $uploader->setDir(__DIR__.'/../../var/uploads');
-        $tmpFile = $uploader->upload('uploaded');
-
-        $this->assertNotNull($tmpFile);
-    }
-
-    public function filesProvider()
-    {
-        return [
-            [
-                'txt',
-                [
-                    'name' => 'patch.txt',
-                    'tmp_name' => $this->fixturesDirectory.'/patch.txt',
-                    'size' => filesize($this->fixturesDirectory.'/patch.txt'),
-                    'error' => UPLOAD_ERR_OK,
-                ]
-            ],
-        ];
-    }
-}
diff --git a/tests/Utils/Versions/ClientTest.php 
b/tests/Utils/Versions/ClientTest.php
deleted file mode 100644
index 54f3ec0..0000000
--- a/tests/Utils/Versions/ClientTest.php
+++ /dev/null
@@ -1,36 +0,0 @@
-<?php
-
-namespace App\Tests\Utils\Versions;
-
-use PHPUnit\Framework\TestCase;
-use App\Utils\Versions\Client;
-
-class ClientTest extends TestCase
-{
-    private $client;
-
-    public function setUp()
-    {
-        $this->client = new Client();
-
-        $reflection = new \ReflectionClass($this->client);
-
-        $devVersionsUrl = $reflection->getProperty('devVersionsUrl');
-        $devVersionsUrl->setAccessible(true);
-        $devVersionsUrl->setValue($this->client, 
__DIR__.'/../../mock/responses/dev-body.txt');
-
-        $stableVersionsUrl = $reflection->getProperty('stableVersionsUrl');
-        $stableVersionsUrl->setAccessible(true);
-        $stableVersionsUrl->setValue($this->client, 
__DIR__.'/../../mock/responses/stable-body.txt');
-    }
-
-    public function testFetchDevVersions()
-    {
-        $this->assertInternalType('array', $this->client->fetchDevVersions());
-    }
-
-    public function testFetchStableVersions()
-    {
-        $this->assertInternalType('array', 
$this->client->fetchStableVersions());
-    }
-}
diff --git a/tests/Utils/Versions/GeneratorTest.php 
b/tests/Utils/Versions/GeneratorTest.php
deleted file mode 100644
index 386a5b6..0000000
--- a/tests/Utils/Versions/GeneratorTest.php
+++ /dev/null
@@ -1,70 +0,0 @@
-<?php
-
-namespace App\Tests\Utils\Versions;
-
-use PHPUnit\Framework\TestCase;
-use App\Utils\Versions\Generator;
-use App\Utils\Versions\Client;
-use App\Utils\Cache;
-
-class GeneratorTest extends TestCase
-{
-    private $cacheDir = __DIR__.'/../../../var/cache/test';
-    private $cache;
-    private $client;
-    private $generator;
-
-    public function setUp()
-    {
-        $this->cache = new Cache($this->cacheDir);
-        $this->cache->clear();
-
-        // The results returned by the client depend on the remote URLs so we
-        // mock the returned results.
-        $this->client = $this->getMockBuilder(Client::class)
-            ->setMethods(['fetchDevVersions', 'fetchStableVersions'])
-            ->getMock();
-
-        $this->client->expects($this->once())
-            ->method('fetchDevVersions')
-            
->will($this->returnValue(json_decode(file_get_contents(__DIR__.'/../../mock/responses/dev-body.txt',
 true))));
-
-        $this->client->expects($this->once())
-            ->method('fetchStableVersions')
-            
->will($this->returnValue(json_decode(file_get_contents(__DIR__.'/../../mock/responses/stable-body.txt'),
 true)));
-
-        $this->generator = $this->getMockBuilder(Generator::class)
-            ->setConstructorArgs([$this->client, $this->cache])
-            ->setMethods(['getAffixes'])
-            ->getMock();
-
-        // The extra versions are always date dependant so we mock it to 
include
-        // static date done on the tests day.
-        $date = '2018-12-26';
-        $this->generator->expects($this->any())
-            ->method('getAffixes')
-            ->will($this->returnValue(['Git-'.$date.' (snap)', 'Git-'.$date.' 
(Git)',]));
-    }
-
-    public function tearDown()
-    {
-        $this->cache->clear();
-        rmdir($this->cacheDir);
-    }
-
-    public function testVersions()
-    {
-        $versions = $this->generator->getVersions();
-
-        $this->assertInternalType('array', $versions);
-        $this->assertGreaterThan(5, count($versions));
-
-        $fixture = require __DIR__.'/../../fixtures/versions/versions.php';
-        $cached = require $this->cacheDir.'/versions.php';
-
-        $this->assertEquals($fixture[1], $cached[1]);
-        $this->assertContains('Next Major Version', $versions);
-        $this->assertContains('Irrelevant', $versions);
-        $this->assertContains('7.2.14RC1', $versions);
-    }
-}
diff --git a/tests/bootstrap.php b/tests/bootstrap.php
new file mode 100644
index 0000000..f6b6db9
--- /dev/null
+++ b/tests/bootstrap.php
@@ -0,0 +1,7 @@
+<?php declare(strict_types=1);
+
+require_once __DIR__ . '/../vendor/autoload.php';
+
+define('TEST_FIXTURES_DIRECTORY', __DIR__ . '/Fixtures');
+define('TEST_MOCKS_DIRECTORY', __DIR__ . '/Mocks');
+define('TEST_VAR_DIRECTORY', __DIR__ . '/../var');
diff --git a/tests/mock/responses/dev-body.txt 
b/tests/mock/responses/dev-body.txt
deleted file mode 100644
index 77697a6..0000000
--- a/tests/mock/responses/dev-body.txt
+++ /dev/null
@@ -1 +0,0 @@
-["5.6.40-dev","7.0.33-dev","7.1.25-dev","7.2.14-dev","7.2.14RC1","7.3.1-dev","7.3.1RC1"]
diff --git a/tests/mock/responses/stable-body.txt 
b/tests/mock/responses/stable-body.txt
deleted file mode 100644
index d4daa41..0000000
--- a/tests/mock/responses/stable-body.txt
+++ /dev/null
@@ -1 +0,0 @@
-{"5":{"5.6":{"announcement":true,"source":[{"filename":"php-5.6.39.tar.bz2","name":"PHP
 5.6.39 
(tar.bz2)","sha256":"b3db2345f50c010b01fe041b4e0f66c5aa28eb325135136f153e18da01583ad5","date":"06
 Dec 2018"},{"filename":"php-5.6.39.tar.gz","name":"PHP 5.6.39 
(tar.gz)","sha256":"127b122b7d6c7f3c211c0ffa554979370c3131196137404a51a391d8e2e9c7bb","date":"06
 Dec 2018"},{"filename":"php-5.6.39.tar.xz","name":"PHP 5.6.39 
(tar.xz)","sha256":"8147576001a832ff3d03cb2980caa2d6b584a10624f87ac459fcd3948c6e4a10","date":"06
 Dec 
2018"}],"version":"5.6.39"}},"7":{"7.0":{"announcement":true,"source":[{"filename":"php-7.0.33.tar.bz2","name":"PHP
 7.0.33 
(tar.bz2)","sha256":"4933ea74298a1ba046b0246fe3771415c84dfb878396201b56cb5333abe86f07","date":"06
 Dec 2018"},{"filename":"php-7.0.33.tar.gz","name":"PHP 7.0.33 
(tar.gz)","sha256":"d71a6ecb6b13dc53fed7532a7f8f949c4044806f067502f8fb6f9facbb40452a","date":"06
 Dec 2018"},{"filename":"php-7.0.33.tar.xz","name":"PHP 7.0.33 
(tar.xz)","sha256":"ab8c5be6e32b1f8d032909dedaaaa4bbb1a209e519abb01a52ce3914f9a13d96","date":"06
 Dec 
2018"}],"version":"7.0.33"},"7.1":{"announcement":true,"source":[{"filename":"php-7.1.25.tar.bz2","name":"PHP
 7.1.25 
(tar.bz2)","sha256":"002cdc880ac7cfaede2c389204d366108847db0f3ac72edf1ba95c0577f9aaac","date":"06
 Dec 2018"},{"filename":"php-7.1.25.tar.gz","name":"PHP 7.1.25 
(tar.gz)","sha256":"7dc40e202140e8b4fb3d992c15a68d98dc06b805e6b218497d260abbe51f5958","date":"06
 Dec 2018"},{"filename":"php-7.1.25.tar.xz","name":"PHP 7.1.25 
(tar.xz)","sha256":"0fd8dad1903cd0b2d615a1fe4209f99e53b7292403c8ffa1919c0f4dd1eada88","date":"06
 Dec 
2018"}],"version":"7.1.25"},"7.2":{"announcement":true,"source":[{"filename":"php-7.2.13.tar.bz2","name":"PHP
 7.2.13 
(tar.bz2)","sha256":"5b4a46fb76491bcd3eee1213773382e570f6ecf9b22d623b24e2822298b3e92d","date":"06
 Dec 2018"},{"filename":"php-7.2.13.tar.gz","name":"PHP 7.2.13 
(tar.gz)","sha256":"e563cee406b1ec96649c22ed2b35796cfe4e9aa9afa6eab6be4cf2fe5d724744","date":"06
 Dec 2018"},{"filename":"php-7.2.13.tar.xz","name":"PHP 7.2.13 
(tar.xz)","sha256":"14b0429abdb46b65c843e5882c9a8c46b31dfbf279c747293b8ab950c2644a4b","date":"06
 Dec 
2018"}],"version":"7.2.13"},"7.3":{"announcement":true,"source":[{"filename":"php-7.3.0.tar.bz2","name":"PHP
 7.3.0 
(tar.bz2)","sha256":"7a267daec9969a997c5c8028c350229646748e0fcc71e2f2dbb157ddcee87c67","date":"06
 Dec 2018"},{"filename":"php-7.3.0.tar.gz","name":"PHP 7.3.0 
(tar.gz)","sha256":"391bd0f91d9bdd01ab47ef9607bad8c65e35bc9bb098fb7777b2556e2c847b11","date":"06
 Dec 2018"},{"filename":"php-7.3.0.tar.xz","name":"PHP 7.3.0 
(tar.xz)","sha256":"7d195cad55af8b288c3919c67023a14ff870a73e3acc2165a6d17a4850a560b5","date":"06
 Dec 2018"}],"version":"7.3.0"}}}
-- 
PHP Webmaster List Mailing List (http://www.php.net/)
To unsubscribe, visit: http://www.php.net/unsub.php

Reply via email to