Commit:    5cd2630a86cd4feaacdd1353f246b234dfe75dc1
Author:    Peter Kokot <peterko...@gmail.com>         Mon, 24 Dec 2018 03:58:58 
+0100
Parents:   102df8157547bb44eeba9e10e646ed0b6ccb48f7
Branches:  master

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

Log:
Add dependency injection container

This patch introduces a dependency injection container for the PHP bug
tracker application. Container deals with the creation of all service
classes and additionally provides retrieving of configuration parameters.
Service classes are used everywhere in the app - from accessing database
to uploading files. Configuration parameters include infrastructure
configuration (database credentials...) and application level
configuration (directories locations...).

Container is compatible with the PSR-11 container interface defined
so it is simple to quickly understand its usage. Advanced features
such as autowiring are not included in this phase.

Changed paths:
  M  README.md
  A  config/container.php
  A  config/parameters.php
  M  docs/README.md
  A  docs/container.md
  M  docs/templates.md
  M  include/prepend.php
  M  include/query.php
  M  scripts/cron/email-assigned
  M  scripts/cron/no-feedback
  A  src/Container/Container.php
  A  src/Container/ContainerInterface.php
  A  src/Container/Exception/ContainerException.php
  A  src/Container/Exception/ContainerExceptionInterface.php
  A  src/Container/Exception/EntryNotFoundException.php
  A  src/Container/Exception/NotFoundExceptionInterface.php
  M  src/Repository/PatchRepository.php
  M  src/Utils/PatchTracker.php
  A  tests/Container/ContainerTest.php
  A  tests/Container/MockDependency.php
  A  tests/Container/MockService.php
  M  www/admin/index.php
  M  www/api.php
  M  www/bug-pwd-finder.php
  M  www/bug.php
  M  www/fix.php
  M  www/gh-pull-add.php
  M  www/index.php
  M  www/lstats.php
  M  www/patch-add.php
  M  www/patch-display.php
  M  www/quick-fix-desc.php
  M  www/report.php
  M  www/rpc.php
  M  www/rss/bug.php
  M  www/stats.php
  M  www/vote.php

diff --git a/README.md b/README.md
index 275f64d..d3421a2 100644
--- a/README.md
+++ b/README.md
@@ -36,6 +36,7 @@ Source code of this application is structured in the 
following directories:
 ```bash
 <web-bugs>/
  ├─ .git/                   # Git configuration and source directory
+ ├─ config/                 # Application configuration parameters, services...
  ├─ docs/                   # Application documentation
  └─ include/                # Application helper functions and configuration
     ├─ prepend.php          # Autoloader, DB connection, container, app 
initialization
diff --git a/config/container.php b/config/container.php
new file mode 100644
index 0000000..5cf2979
--- /dev/null
+++ b/config/container.php
@@ -0,0 +1,84 @@
+<?php
+
+/**
+ * Container initialization. Each service is created using Container::set()
+ * method and a callable argument for convenience of future customizations or
+ * adjustments beyond the scope of this container. See documentation for more
+ * information.
+ */
+
+use App\Container\Container;
+
+$container = new Container(include __DIR__.'/parameters.php');
+
+$container->set(\PDO::class, function ($c) {
+    return new \PDO(
+        
'mysql:host='.$c->get('db_host').';dbname='.$c->get('db_name').';charset=utf8',
+        $c->get('db_user'),
+        $c->get('db_password'),
+        [
+            \PDO::ATTR_ERRMODE            => \PDO::ERRMODE_EXCEPTION,
+            \PDO::ATTR_DEFAULT_FETCH_MODE => \PDO::FETCH_ASSOC,
+            \PDO::ATTR_EMULATE_PREPARES   => false,
+            \PDO::ATTR_STATEMENT_CLASS    => [App\Database\Statement::class],
+        ]
+    );
+});
+
+$container->set(App\Repository\BugRepository::class, function ($c) {
+    return new App\Repository\BugRepository($c->get(\PDO::class));
+});
+
+$container->set(App\Repository\CommentRepository::class, function ($c) {
+    return new App\Repository\CommentRepository($c->get(\PDO::class));
+});
+
+$container->set(App\Repository\ObsoletePatchRepository::class, function ($c) {
+    return new App\Repository\ObsoletePatchRepository($c->get(\PDO::class));
+});
+
+$container->set(App\Repository\PackageRepository::class, function ($c) {
+    return new App\Repository\PackageRepository($c->get(\PDO::class));
+});
+
+$container->set(App\Repository\PatchRepository::class, function ($c) {
+    return new App\Repository\PatchRepository($c->get(\PDO::class), 
$c->get('uploads_dir'));
+});
+
+$container->set(App\Repository\PullRequestRepository::class, function ($c) {
+    return new App\Repository\PullRequestRepository($c->get(\PDO::class));
+});
+
+$container->set(App\Repository\ReasonRepository::class, function ($c) {
+    return new App\Repository\ReasonRepository($c->get(\PDO::class));
+});
+
+$container->set(App\Repository\VoteRepository::class, function ($c) {
+    return new App\Repository\VoteRepository($c->get(\PDO::class));
+});
+
+$container->set(App\Template\Engine::class, function ($c) {
+    return new App\Template\Engine($c->get('templates_dir'));
+});
+
+$container->set(App\Utils\Captcha::class, function ($c) {
+    return new App\Utils\Captcha();
+});
+
+$container->set(App\Utils\GitHub::class, function ($c) {
+    return new App\Utils\GitHub($c->get(\PDO::class));
+});
+
+$container->set(App\Utils\PatchTracker::class, function ($c) {
+    return new App\Utils\PatchTracker(
+        $c->get(\PDO::class),
+        $c->get(App\Utils\Uploader::class),
+        $c->get('uploads_dir')
+    );
+});
+
+$container->set(App\Utils\Uploader::class, function ($c) {
+    return new App\Utils\Uploader();
+});
+
+return $container;
diff --git a/config/parameters.php b/config/parameters.php
new file mode 100644
index 0000000..9ff0d78
--- /dev/null
+++ b/config/parameters.php
@@ -0,0 +1,73 @@
+<?php
+
+/**
+ * Application configuration parameters.
+ */
+
+return [
+    /**
+     * Application environment. Can be one of ['dev', 'prod'].
+     */
+    'env' => (defined('DEVBOX') && true === DEVBOX) ? 'dev' : 'prod',
+
+    /**
+     * Site scheme - http or https.
+     */
+    'site_scheme' => $site_data['method'],
+
+    /**
+     * Site URL.
+     */
+    'site_url' => $site_data['url'],
+
+    /**
+     * Site base path if present. Part that comes after the domain
+     * https://bugs.php.net/basedir/
+     */
+    'basedir' => $site_data['basedir'],
+
+    /**
+     * Database username.
+     */
+    'db_user' => $site_data['db_user'],
+
+    /**
+     * Database password.
+     */
+    'db_password' => $site_data['db_pass'],
+
+    /**
+     * Database host name.
+     */
+    'db_host' => $site_data['db_host'],
+
+    /**
+     * Database name.
+     */
+    'db_name' => $site_data['db'],
+
+    /**
+     * Main email of the public mailing list.
+     */
+    'email'=> $site_data['email'],
+
+    /**
+     * Email of the public mailing list for documentation related bugs.
+     */
+    'doc_email' => $site_data['doc_email'],
+
+    /**
+     * Security email - emails sent to this are not visible in public.
+     */
+    'security_email' => $site_data['security_email'],
+
+    /**
+     * Uploads directory location.
+     */
+    'uploads_dir' => $site_data['patch_tmp'],
+
+    /**
+     * Templates directory.
+     */
+    'templates_dir' => __DIR__.'/../templates',
+];
diff --git a/docs/README.md b/docs/README.md
index 1cc4c00..273ab36 100644
--- a/docs/README.md
+++ b/docs/README.md
@@ -1,3 +1,4 @@
 # Application documentation
 
 * [Templates](/docs/templates.md)
+* [Dependency injection container](/docs/container.md)
diff --git a/docs/container.md b/docs/container.md
new file mode 100644
index 0000000..2403f57
--- /dev/null
+++ b/docs/container.md
@@ -0,0 +1,119 @@
+# Dependency injection container
+
+The PHP bug tracker application ships with a simplistic dependency injection
+container which can create services and retrieves configuration values.
+
+Services are one of the more frequently used objects everywhere across the
+application. For example, service for database access, utility service for
+uploading files, data generators, API clients, and similar.
+
+## Dependency injection
+
+Dependencies between classes are injected using either constructor, or via a
+method call such as setter.
+
+```php
+class Repository
+{
+    private $pdo;
+
+    public function __construct(\PDO $pdo)
+    {
+        $this->pdo = $pdo;
+    }
+
+    public function getData(): array
+    {
+        return $this->pdo->query("SELECT * FROM table")->fetchAll();
+    }
+}
+
+$pdo = new \PDO(
+    'mysql:host=localhost;dbname=bugs;charset=utf8', 'nobody', 'password',
+    [
+        \PDO::ATTR_ERRMODE            => \PDO::ERRMODE_EXCEPTION,
+        \PDO::ATTR_DEFAULT_FETCH_MODE => \PDO::FETCH_ASSOC,
+        \PDO::ATTR_EMULATE_PREPARES   => false,
+    ]
+);
+
+$repository = new Repository($pdo);
+$data = $repository->getData();
+```
+
+The `$pdo` object in the example is a dependency which is injected via the
+constructor.
+
+Dependency injection container further provides a more efficient creation of
+such dependencies and services:
+
+```php
+$container = require_once __DIR__.'/../config/container.php';
+
+$data = $container->get(Repository::class)->getData();
+```
+
+## Configuration
+
+Configuration parameters include infrastructure configuration (database
+credentials...) and application level configuration (directories locations...).
+
+```php
+// config/parameters.php
+
+return [
+    'parameter_key' => 'value',
+
+    // ...
+];
+```
+
+Which can be retrieved by the container:
+
+```php
+$value = $container->get('parameter_key');
+```
+
+## Container definitions
+
+Each service class is manually defined:
+
+```php
+// config/container.php
+
+// New container initialization with configuration parameters defined in a 
file.
+$container = new Container(include __DIR__.'/parameters.php');
+
+// Services are then defined using callables with a container argument $c.
+
+// Service with constructor arguments
+$container->set(App\Foo::class, function ($c) {
+    return new App\Foo($c->get(App\Dependency::class));
+});
+
+// Service with a setter method
+$container->set(App\Foo\Bar::class, function ($c) {
+    $object = new App\Foo\Bar($c->get(App\Dependency::class));
+    $object->setFoobar('argument');
+
+    return $object;
+});
+
+// Dependencies can be service classes or configuration parameters
+$container->set(App\Foo\Bar::class, function ($c) {
+    return new App\Foo\Bar(
+        // Configuration parameter
+        $c->get('parameter_key'));
+
+        // Calling method from another service
+        $c->get(App\Dependency::class)->methodName();
+    );
+});
+
+// Service with no dependencies
+$container->set(App\Dependency::class, function ($c) {
+    return new App\Dependency();
+});
+
+return $container;
+```
diff --git a/docs/templates.md b/docs/templates.md
index 66ca34d..cea0976 100644
--- a/docs/templates.md
+++ b/docs/templates.md
@@ -4,7 +4,7 @@ A simple template engine separates logic from the presentation 
and provides
 methods for creating nested templates and escaping strings to protect against
 too common XSS vulnerabilities.
 
-It is initialized in the application bootstrap:
+Template engine initialization:
 
 ```php
 $template = new App\Template\Engine(__DIR__.'/../path/to/templates');
diff --git a/include/prepend.php b/include/prepend.php
index 49400f9..64b99ff 100644
--- a/include/prepend.php
+++ b/include/prepend.php
@@ -1,7 +1,6 @@
 <?php
 
 use App\Autoloader;
-use App\Database\Statement;
 use App\Template\Engine;
 
 // Dual PSR-4 compatible class autoloader. When Composer is not available, an
@@ -26,40 +25,33 @@ if (file_exists(__DIR__.'/../vendor/autoload.php')) {
     $loader->addClassmap('Horde_Text_Diff_Renderer', 
__DIR__.'/../src/Horde/Text/Diff/Renderer.php');
 }
 
+// Configuration
 $site = 'php';
 $siteBig = 'PHP';
 $ROOT_DIR = realpath(__DIR__ . '/../');
 
-$local_cfg = "{$ROOT_DIR}/local_config.php";
-if (file_exists($local_cfg)) {
-       require $local_cfg;
+$localConfigFile = __DIR__.'/../local_config.php';
+if (file_exists($localConfigFile)) {
+    require $localConfigFile;
 } else {
-       $site_data = [
-               'method' => 'https',
-               'url' => 'bugs.php.net',
-               'basedir' => '',
-               'email' => 'php-b...@lists.php.net',
-               'doc_email' => 'doc-b...@lists.php.net',
-               'security_email' => 'secur...@php.net',
-               'db' => 'phpbugsdb',
-               'db_user' => 'nobody',
-               'db_pass' => '',
-               'db_host' => 'localhost',
-               'patch_tmp' => "{$ROOT_DIR}/uploads/patches/",
-       ];
-       define('DEVBOX', false);
-}
-// CONFIG END
-
-// Configure errors based on the environment.
-if (defined('DEVBOX') && true === DEVBOX) {
-    ini_set('display_errors', 1);
-} else {
-    ini_set('display_errors', 0);
+    $site_data = [
+        'method' => 'https',
+        'url' => 'bugs.php.net',
+        'basedir' => '',
+        'email' => 'php-b...@lists.php.net',
+        'doc_email' => 'doc-b...@lists.php.net',
+        'security_email' => 'secur...@php.net',
+        'db' => 'phpbugsdb',
+        'db_user' => 'nobody',
+        'db_pass' => '',
+        'db_host' => 'localhost',
+        'patch_tmp' => __DIR__.'/../uploads/patches/',
+    ];
+    define('DEVBOX', false);
 }
 
 if (!isset($site_data['security_email'])) {
-       $site_data['security_email'] = 'secur...@php.net';
+    $site_data['security_email'] = 'secur...@php.net';
 }
 
 // DO NOT EDIT ANYTHING BELOW THIS LINE, edit $site_data above instead!
@@ -70,34 +62,31 @@ $bugEmail = $site_data['email'];
 $docBugEmail = $site_data['doc_email'];
 $secBugEmail = $site_data['security_email'];
 $basedir = $site_data['basedir'];
-define('BUG_PATCHTRACKER_TMPDIR', $site_data['patch_tmp']);
 
-/**
- * Obtain the functions and variables used throughout the bug system
- */
-require_once "{$ROOT_DIR}/include/functions.php";
+// Container initialization
+$container = require_once __DIR__.'/../config/container.php';
+
+// Configure errors based on the environment.
+if ('dev' === $container->get('env')) {
+    ini_set('display_errors', '1');
+} else {
+    ini_set('display_errors', '0');
+}
+
+// Obtain the functions and variables used throughout the bug system.
+require_once __DIR__.'/functions.php';
 
-// Database connection with vanilla PDO to understand app architecture in no 
time
-$dbh = new \PDO(
-    
'mysql:host='.$site_data['db_host'].';dbname='.$site_data['db'].';charset=utf8',
-    $site_data['db_user'],
-    $site_data['db_pass'],
-    [
-        \PDO::ATTR_ERRMODE            => \PDO::ERRMODE_EXCEPTION,
-        \PDO::ATTR_DEFAULT_FETCH_MODE => \PDO::FETCH_ASSOC,
-        \PDO::ATTR_EMULATE_PREPARES   => false,
-        \PDO::ATTR_STATEMENT_CLASS    => [Statement::class],
-    ]
-);
+// Database connection with vanilla PDO to understand app architecture in no 
time.
+$dbh = $container->get(\PDO::class);
 
-// Last Updated..
+// Last updated.
 $tmp = filectime($_SERVER['SCRIPT_FILENAME']);
 $LAST_UPDATED = date('D M d H:i:s Y', $tmp - date('Z', $tmp)) . ' UTC';
 
 // Initialize template engine.
-$template = new Engine(__DIR__.'/../templates');
+$template = $container->get(Engine::class);
 $template->assign([
     'lastUpdated' => $LAST_UPDATED,
-    'siteScheme'  => $site_method,
-    'siteUrl'     => $site_url,
+    'siteScheme'  => $container->get('site_scheme'),
+    'siteUrl'     => $container->get('site_url'),
 ]);
diff --git a/include/query.php b/include/query.php
index 51ed807..efe903b 100644
--- a/include/query.php
+++ b/include/query.php
@@ -22,7 +22,7 @@ $order_options = [
 ];
 
 // Fetch pseudo packages
-$packageRepository = new PackageRepository($dbh);
+$packageRepository = $container->get(PackageRepository::class);
 $pseudo_pkgs = $packageRepository->findAll();
 
 // Setup input variables..
diff --git a/scripts/cron/email-assigned b/scripts/cron/email-assigned
index 60456a6..e841962 100755
--- a/scripts/cron/email-assigned
+++ b/scripts/cron/email-assigned
@@ -10,7 +10,7 @@ require __DIR__ . '/../../include/prepend.php';
   is a little odd that way.
   'No Feedback' was once a part of this, but no longer: 
https://news.php.net/php.webmaster/8828
 */
-foreach ((new BugRepository($dbh))->findAllAssigned() as $assigned => $binfos) 
{
+foreach ($container->get(BugRepository::class)->findAllAssigned() as $assigned 
=> $binfos) {
 
        $mbody      = format_email_body($binfos);
        $email_user = $assigned . '@php.net';
diff --git a/scripts/cron/no-feedback b/scripts/cron/no-feedback
index 653ab8d..82c3deb 100755
--- a/scripts/cron/no-feedback
+++ b/scripts/cron/no-feedback
@@ -12,11 +12,11 @@ require __DIR__.'/../../include/prepend.php';
 $in = ['status' => 'No Feedback'];
 
 # Update relevant reports
-$reasonRepository = new ReasonRepository($dbh);
+$reasonRepository = $container->get(ReasonRepository::class);
 
 list($RESOLVE_REASONS, $FIX_VARIATIONS) = 
$reasonRepository->findByProject($site);
 
-foreach ((new BugRepository($dbh))->findAllWithoutFeedback() as $bug)
+foreach ($container->get(BugRepository::class)->findAllWithoutFeedback() as 
$bug)
 {
        list($mailto, $mailfrom, $bcc, $params) = 
get_package_mail($bug['package_name'], false, $bug['bug_type']);
 
diff --git a/src/Container/Container.php b/src/Container/Container.php
new file mode 100644
index 0000000..ebf2fad
--- /dev/null
+++ b/src/Container/Container.php
@@ -0,0 +1,101 @@
+<?php
+
+namespace App\Container;
+
+use App\Container\Exception\EntryNotFoundException;
+use App\Container\Exception\ContainerException;
+
+/**
+ * PSR-11 compatible dependency injection container.
+ */
+class Container implements ContainerInterface
+{
+    /**
+     * All defined services and parameters.
+     *
+     * @var array
+     */
+    public $entries = [];
+
+    /**
+     * Already retrieved items are stored for faster retrievals in the same 
run.
+     *
+     * @var array
+     */
+    private $store = [];
+
+    /**
+     * Services already created to prevent circular references.
+     *
+     * @var array
+     */
+    private $locks = [];
+
+    /**
+     * Class constructor.
+     */
+    public function __construct(array $configurations = [])
+    {
+        $this->entries = $configurations;
+    }
+
+    /**
+     * Set service.
+     */
+    public function set(string $key, $entry): void
+    {
+        $this->entries[$key] = $entry;
+    }
+
+    /**
+     * Get entry.
+     *
+     * @return mixed
+     */
+    public function get(string $id)
+    {
+        if (!$this->has($id)) {
+            throw new EntryNotFoundException($id.' entry not found.');
+        }
+
+        if (!isset($this->store[$id])) {
+            $this->store[$id] = $this->createEntry($id);
+        }
+
+        return $this->store[$id];
+    }
+
+    /**
+     * Check if entry is available in the container.
+     */
+    public function has(string $id): bool
+    {
+        return isset($this->entries[$id]);
+    }
+
+    /**
+     * Create new entry - service or configuration parameter.
+     *
+     * @return mixed
+     */
+    private function createEntry(string $id)
+    {
+        $entry = &$this->entries[$id];
+
+        // Entry is a configuration parameter.
+        if (!class_exists($id) && !is_callable($entry)) {
+            return $entry;
+        }
+
+        // Entry is a service.
+        if (class_exists($id) && !is_callable($entry)) {
+            throw new ContainerException($id.' entry must be callable.');
+        } elseif (class_exists($id) && isset($this->locks[$id])) {
+            throw new ContainerException($id.' entry contains a circular 
reference.');
+        }
+
+        $this->locks[$id] = true;
+
+        return $entry($this);
+    }
+}
diff --git a/src/Container/ContainerInterface.php 
b/src/Container/ContainerInterface.php
new file mode 100644
index 0000000..211ba7e
--- /dev/null
+++ b/src/Container/ContainerInterface.php
@@ -0,0 +1,34 @@
+<?php
+
+namespace App\Container;
+
+/**
+ * Describes the interface of a container that exposes methods to read its 
entries.
+ */
+interface ContainerInterface
+{
+    /**
+     * Finds an entry of the container by its identifier and returns it.
+     *
+     * @param string $id identifier of the entry to look for
+     *
+     * @throws NotFoundExceptionInterface  no entry was found for **this** 
identifier
+     * @throws ContainerExceptionInterface error while retrieving the entry
+     *
+     * @return mixed entry
+     */
+    public function get(string $id);
+
+    /**
+     * Returns true if the container can return an entry for the given 
identifier.
+     * Returns false otherwise.
+     *
+     * `has($id)` returning true does not mean that `get($id)` will not throw 
an exception.
+     * It does however mean that `get($id)` will not throw a 
`NotFoundExceptionInterface`.
+     *
+     * @param string $id identifier of the entry to look for
+     *
+     * @return bool
+     */
+    public function has(string $id): bool;
+}
diff --git a/src/Container/Exception/ContainerException.php 
b/src/Container/Exception/ContainerException.php
new file mode 100644
index 0000000..4a571b8
--- /dev/null
+++ b/src/Container/Exception/ContainerException.php
@@ -0,0 +1,10 @@
+<?php
+
+namespace App\Container\Exception;
+
+/**
+ * Generic exception in a container.
+ */
+class ContainerException extends \Exception implements 
ContainerExceptionInterface
+{
+}
diff --git a/src/Container/Exception/ContainerExceptionInterface.php 
b/src/Container/Exception/ContainerExceptionInterface.php
new file mode 100644
index 0000000..261fcb0
--- /dev/null
+++ b/src/Container/Exception/ContainerExceptionInterface.php
@@ -0,0 +1,10 @@
+<?php
+
+namespace App\Container\Exception;
+
+/**
+ * Base interface representing a generic exception in a container.
+ */
+interface ContainerExceptionInterface
+{
+}
diff --git a/src/Container/Exception/EntryNotFoundException.php 
b/src/Container/Exception/EntryNotFoundException.php
new file mode 100644
index 0000000..df5ee07
--- /dev/null
+++ b/src/Container/Exception/EntryNotFoundException.php
@@ -0,0 +1,7 @@
+<?php
+
+namespace App\Container\Exception;
+
+class EntryNotFoundException extends \Exception implements 
NotFoundExceptionInterface
+{
+}
diff --git a/src/Container/Exception/NotFoundExceptionInterface.php 
b/src/Container/Exception/NotFoundExceptionInterface.php
new file mode 100644
index 0000000..b19d953
--- /dev/null
+++ b/src/Container/Exception/NotFoundExceptionInterface.php
@@ -0,0 +1,10 @@
+<?php
+
+namespace App\Container\Exception;
+
+/**
+ * No entry was found in the container.
+ */
+interface NotFoundExceptionInterface extends ContainerExceptionInterface
+{
+}
diff --git a/src/Repository/PatchRepository.php 
b/src/Repository/PatchRepository.php
index 9fd9031..ff0af79 100644
--- a/src/Repository/PatchRepository.php
+++ b/src/Repository/PatchRepository.php
@@ -22,10 +22,10 @@ class PatchRepository
     /**
      * Class constructor.
      */
-    public function __construct(\PDO $dbh)
+    public function __construct(\PDO $dbh, string $uploadsDir)
     {
         $this->dbh = $dbh;
-        $this->uploadsDir = BUG_PATCHTRACKER_TMPDIR;
+        $this->uploadsDir = $uploadsDir;
     }
 
     /**
diff --git a/src/Utils/PatchTracker.php b/src/Utils/PatchTracker.php
index 028f9f1..0e080f4 100644
--- a/src/Utils/PatchTracker.php
+++ b/src/Utils/PatchTracker.php
@@ -2,8 +2,6 @@
 
 namespace App\Utils;
 
-use App\Utils\Uploader;
-
 /**
  * Service for handling uploaded patches.
  */
@@ -48,10 +46,10 @@ class PatchTracker
     /**
      * Class constructor.
      */
-    public function __construct(\PDO $dbh, Uploader $uploader)
+    public function __construct(\PDO $dbh, Uploader $uploader, string 
$uploadsDir)
     {
         $this->dbh = $dbh;
-        $this->uploadsDir = BUG_PATCHTRACKER_TMPDIR;
+        $this->uploadsDir = $uploadsDir;
 
         $this->uploader = $uploader;
         $this->uploader->setMaxFileSize(self::MAX_FILE_SIZE);
diff --git a/tests/Container/ContainerTest.php 
b/tests/Container/ContainerTest.php
new file mode 100644
index 0000000..42e1a74
--- /dev/null
+++ b/tests/Container/ContainerTest.php
@@ -0,0 +1,109 @@
+<?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
new file mode 100644
index 0000000..dc2c3a9
--- /dev/null
+++ b/tests/Container/MockDependency.php
@@ -0,0 +1,21 @@
+<?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
new file mode 100644
index 0000000..7d12d39
--- /dev/null
+++ b/tests/Container/MockService.php
@@ -0,0 +1,32 @@
+<?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/www/admin/index.php b/www/admin/index.php
index 1c2c810..c71b90b 100644
--- a/www/admin/index.php
+++ b/www/admin/index.php
@@ -53,7 +53,7 @@ if ($action === 'phpinfo') {
 
 } elseif ($action === 'list_lists') {
        echo "<dl>\n";
-       foreach ((new PackageRepository($dbh))->findLists() as $row) {
+       foreach ($container->get(PackageRepository::class)->findLists() as 
$row) {
                echo "<dt>", $row['name'], ": </dt>\n<dd>", 
mailto_list(explode(',', $row['list_email'])), "</dd>\n";
        }
        echo "</dl>\n";
diff --git a/www/api.php b/www/api.php
index 4fbf5bc..9cdc36e 100644
--- a/www/api.php
+++ b/www/api.php
@@ -15,7 +15,7 @@ $action   = isset($_GET['action'])   ? $_GET['action']        
 : 'unknown';
 $interval = isset($_GET['interval']) ? (int) $_GET['interval'] : 7;
 
 if ($type === 'docs' && $action === 'closed' && $interval) {
-       $commentRepository = new CommentRepository($dbh);
+       $commentRepository = $container->get(CommentRepository::class);
        $rows = $commentRepository->findDocsComments($interval);
 
        //@todo add error handling
diff --git a/www/bug-pwd-finder.php b/www/bug-pwd-finder.php
index f0aa2ba..5e3fe00 100644
--- a/www/bug-pwd-finder.php
+++ b/www/bug-pwd-finder.php
@@ -10,7 +10,7 @@ require_once '../include/prepend.php';
 // Start session (for captcha!)
 session_start();
 
-$captcha = new Captcha();
+$captcha = $container->get(Captcha::class);
 
 $errors  = [];
 $success = false;
diff --git a/www/bug.php b/www/bug.php
index 0fae892..e89acb8 100644
--- a/www/bug.php
+++ b/www/bug.php
@@ -16,8 +16,8 @@ require_once '../include/prepend.php';
 // Start session
 session_start();
 
-$obsoletePatchRepository = new ObsoletePatchRepository($dbh);
-$patchRepository = new PatchRepository($dbh);
+$obsoletePatchRepository = $container->get(ObsoletePatchRepository::class);
+$patchRepository = $container->get(PatchRepository::class);
 
 define('SPAM_REJECT_MESSAGE', 'Your comment looks like SPAM by its content. 
Please consider rewording.');
 $email = null;
@@ -125,14 +125,14 @@ if ($edit == 1 && $is_trusted_developer && 
isset($_GET['delete_comment'])) {
 
 // captcha is not necessary if the user is logged in
 if (!$logged_in) {
-       $captcha = new Captcha();
+       $captcha = $container->get(Captcha::class);
 }
 
 $trytoforce = isset($_POST['trytoforce']) ? (int) $_POST['trytoforce'] : 0;
 
 // fetch info about the bug into $bug
 if (!isset($bug)) {
-       $bugRepository = new BugRepository($dbh);
+       $bugRepository = $container->get(BugRepository::class);
        $bug = $bugRepository->findOneById($bug_id);
 }
 
@@ -187,13 +187,13 @@ $project = $bug['project'];
 
 // Only fetch stuff when it's really needed
 if ($edit && $edit < 3) {
-       $packageRepository = new PackageRepository($dbh);
+       $packageRepository = $container->get(PackageRepository::class);
        $pseudo_pkgs = $packageRepository->findEnabled();
 }
 
 // Fetch RESOLVE_REASONS array
 if ($edit === 1) {
-       $reasonRepository = new ReasonRepository($dbh);
+       $reasonRepository = $container->get(ReasonRepository::class);
        list($RESOLVE_REASONS, $FIX_VARIATIONS) = 
$reasonRepository->findByProject($project);
 }
 
@@ -1096,7 +1096,7 @@ OUTPUT;
        }
        echo "<p><a href='patch-add.php?bug_id={$bug_id}'>Add a Patch</a></p>";
 
-       $pullRequestRepository = new PullRequestRepository($dbh);
+       $pullRequestRepository = $container->get(PullRequestRepository::class);
        $pulls = $pullRequestRepository->findAllByBugId($bug_id);
        echo "<h2>Pull Requests</h2>\n";
 
@@ -1105,7 +1105,7 @@ OUTPUT;
 }
 
 // Display comments
-$commentRepository = new CommentRepository($dbh);
+$commentRepository = $container->get(CommentRepository::class);
 $bug_comments = is_int($bug_id) ? $commentRepository->findByBugId($bug_id) : 
[];
 
 if ($show_bug_info && is_array($bug_comments) && count($bug_comments) && 
$bug['status'] !== 'Spam') {
diff --git a/www/fix.php b/www/fix.php
index 66f552d..4a11487 100644
--- a/www/fix.php
+++ b/www/fix.php
@@ -20,7 +20,7 @@ if (!$bug_id) {
 bugs_authenticate($user, $pw, $logged_in, $user_flags);
 
 // fetch info about the bug into $bug
-$bugRepository = new BugRepository($dbh);
+$bugRepository = $container->get(BugRepository::class);
 $bug = $bugRepository->findOneById($bug_id);
 
 if (!is_array($bug)) {
@@ -39,7 +39,7 @@ if ($logged_in != 'developer') {
 
 $project = !empty($_GET['project']) ? $_GET['project'] : false;
 
-$reasonRepository = new ReasonRepository($dbh);
+$reasonRepository = $container->get(ReasonRepository::class);
 list($RESOLVE_REASONS, $FIX_VARIATIONS) = 
$reasonRepository->findByProject($site);
 
 // Handle reason / comments
diff --git a/www/gh-pull-add.php b/www/gh-pull-add.php
index 4e31f5f..3f1d91a 100644
--- a/www/gh-pull-add.php
+++ b/www/gh-pull-add.php
@@ -27,7 +27,7 @@ if (empty($bug_id)) {
        exit;
 }
 
-$bugRepository = new BugRepository($dbh);
+$bugRepository = $container->get(BugRepository::class);
 
 if (!($buginfo = $bugRepository->findOneById($bug_id))) {
        response_header('Error :: invalid bug selected');
@@ -40,7 +40,7 @@ $package_name = $buginfo['package_name'];
 
 // captcha is not necessary if the user is logged in
 if (!$logged_in) {
-       $captcha = new Captcha();
+       $captcha = $container->get(Captcha::class);
 }
 
 $show_bug_info = bugs_has_access($bug_id, $buginfo, $pw, $user_flags);
@@ -52,8 +52,8 @@ if (!$show_bug_info) {
        exit;
 }
 
-$pullinfo = new GitHub($dbh);
-$pullRequestRepository = new PullRequestRepository($dbh);
+$pullinfo = $container->get(GitHub::class);
+$pullRequestRepository = $container->get(PullRequestRepository::class);
 
 if (isset($_POST['addpull'])) {
        $errors = [];
@@ -104,7 +104,7 @@ if (isset($_POST['addpull'])) {
                                $errors[] = 'This pull request is already 
added.';
                        }
 
-                       if (DEVBOX) {
+                       if ('dev' === $container->get('env')) {
                                $errors[] = $e->getMessage();
                        }
                }
diff --git a/www/index.php b/www/index.php
index 413e608..34b69c9 100644
--- a/www/index.php
+++ b/www/index.php
@@ -40,7 +40,7 @@ if (0 !== $id) {
 }
 
 if ('/random' === $_SERVER['REQUEST_URI']) {
-    $id = (new BugRepository($dbh))->findRandom();
+    $id = $container->get(BugRepository::class)->findRandom();
     redirect('bug.php?id='.$id[0]);
 }
 
diff --git a/www/lstats.php b/www/lstats.php
index 2a22c4c..765d44c 100644
--- a/www/lstats.php
+++ b/www/lstats.php
@@ -46,7 +46,7 @@ if (!$phpver || ($phpver !== 5 && $phpver !== 7)) {
 
 if (isset($_GET['per_category']))
 {
-       $packageRepository = new PackageRepository($dbh);
+       $packageRepository = $container->get(PackageRepository::class);
        $pseudo_pkgs = $packageRepository->findAll($_GET['project'] ?? '');
 
        $totals = [];
diff --git a/www/patch-add.php b/www/patch-add.php
index 6be26ba..76e34f0 100644
--- a/www/patch-add.php
+++ b/www/patch-add.php
@@ -4,14 +4,12 @@ use App\Repository\BugRepository;
 use App\Repository\PatchRepository;
 use App\Utils\Captcha;
 use App\Utils\PatchTracker;
-use App\Utils\Uploader;
 
 // Obtain common includes
 require_once '../include/prepend.php';
 
-$uploader = new Uploader();
-$patchTracker = new PatchTracker($dbh, $uploader);
-$patchRepository = new PatchRepository($dbh);
+$patchTracker = $container->get(PatchTracker::class);
+$patchRepository = $container->get(PatchRepository::class);
 
 session_start();
 
@@ -33,7 +31,7 @@ if (empty($bug_id)) {
        exit;
 }
 
-$bugRepository = new BugRepository($dbh);
+$bugRepository = $container->get(BugRepository::class);
 
 if (!($buginfo = $bugRepository->findOneById($bug_id))) {
        response_header('Error :: invalid bug selected');
@@ -46,7 +44,7 @@ $package_name = $buginfo['package_name'];
 
 // captcha is not necessary if the user is logged in
 if (!$logged_in) {
-       $captcha = new Captcha();
+       $captcha = $container->get(Captcha::class);
 }
 
 $show_bug_info = bugs_has_access($bug_id, $buginfo, $pw, $user_flags);
diff --git a/www/patch-display.php b/www/patch-display.php
index 5a027bb..de2b993 100644
--- a/www/patch-display.php
+++ b/www/patch-display.php
@@ -4,7 +4,6 @@ use App\Repository\BugRepository;
 use App\Repository\ObsoletePatchRepository;
 use App\Repository\PatchRepository;
 use App\Utils\PatchTracker;
-use App\Utils\Uploader;
 use App\Utils\Diff;
 
 session_start();
@@ -12,10 +11,9 @@ session_start();
 // Obtain common includes
 require_once '../include/prepend.php';
 
-$obsoletePatchRepository = new ObsoletePatchRepository($dbh);
-$patchRepository = new PatchRepository($dbh);
-$uploader = new Uploader();
-$patchTracker = new PatchTracker($dbh, $uploader);
+$obsoletePatchRepository = $container->get(ObsoletePatchRepository::class);
+$patchRepository = $container->get(PatchRepository::class);
+$patchTracker = $container->get(PatchTracker::class);
 
 // Authenticate
 bugs_authenticate($user, $pw, $logged_in, $user_flags);
@@ -40,7 +38,7 @@ if (empty($bug_id)) {
        $bug_id = (int) $_GET['bug_id'];
 }
 
-$bugRepository = new BugRepository($dbh);
+$bugRepository = $container->get(BugRepository::class);
 
 if (!($buginfo = $bugRepository->findOneById($bug_id))) {
        response_header('Error :: invalid bug selected');
diff --git a/www/quick-fix-desc.php b/www/quick-fix-desc.php
index 01ff098..208f771 100644
--- a/www/quick-fix-desc.php
+++ b/www/quick-fix-desc.php
@@ -7,7 +7,7 @@ session_start();
 // Obtain common includes
 require_once '../include/prepend.php';
 
-$reasonRepository = new ReasonRepository($dbh);
+$reasonRepository = $container->get(ReasonRepository::class);
 list($RESOLVE_REASONS, $FIX_VARIATIONS) = 
$reasonRepository->findByProject($site);
 
 // Authenticate
diff --git a/www/report.php b/www/report.php
index 9a7b61b..dc55609 100644
--- a/www/report.php
+++ b/www/report.php
@@ -5,7 +5,6 @@ use App\Repository\ReasonRepository;
 use App\Utils\Cache;
 use App\Utils\Captcha;
 use App\Utils\PatchTracker;
-use App\Utils\Uploader;
 use App\Utils\Versions\Client;
 use App\Utils\Versions\Generator;
 
@@ -19,7 +18,7 @@ session_start();
 $errors = [];
 $ok_to_submit_report = false;
 
-$packageRepository = new PackageRepository($dbh);
+$packageRepository = $container->get(PackageRepository::class);
 $pseudo_pkgs = $packageRepository->findEnabled($_GET['project'] ?? '');
 
 // Authenticate
@@ -33,7 +32,7 @@ $versions = $versionsGenerator->getVersions();
 
 // captcha is not necessary if the user is logged in
 if (!$logged_in) {
-       $captcha = new Captcha();
+       $captcha = $container->get(Captcha::class);
 }
 
 $packageAffectedScript = <<<SCRIPT
@@ -233,8 +232,7 @@ OUTPUT;
 
                        $redirectToPatchAdd = false;
                        if (!empty($_POST['in']['patchname']) && 
$_POST['in']['patchname']) {
-                               $uploader = new Uploader();
-                               $tracker = new PatchTracker($dbh, $uploader);
+                               $tracker = $container->get(PatchTracker::class);
 
                                try {
                                        $developer = 
!empty($_POST['in']['handle']) ? $_POST['in']['handle'] : $_POST['in']['email'];
@@ -284,7 +282,7 @@ REPORT;
                        }
 
                        // provide shortcut URLS for "quick bug fixes"
-                       $reasonRepository = new ReasonRepository($dbh);
+                       $reasonRepository = 
$container->get(ReasonRepository::class);
                        list($RESOLVE_REASONS, $FIX_VARIATIONS) = 
$reasonRepository->findByProject($_GET['project'] ?? '');
 
                        $dev_extra = '';
diff --git a/www/rpc.php b/www/rpc.php
index dca690a..94ce340 100644
--- a/www/rpc.php
+++ b/www/rpc.php
@@ -37,7 +37,7 @@ if (empty($auth_user->handle)) {
 }
 
 // fetch info about the bug into $bug
-$bugRepository = new BugRepository($dbh);
+$bugRepository = $container->get(BugRepository::class);
 $bug = $bugRepository->findOneById($bug_id);
 
 if (!is_array($bug)) {
diff --git a/www/rss/bug.php b/www/rss/bug.php
index 2198161..37a6b9f 100644
--- a/www/rss/bug.php
+++ b/www/rss/bug.php
@@ -16,7 +16,7 @@ require_once '../../include/prepend.php';
 $bug_id = isset($_REQUEST['id']) ? (int)$_REQUEST['id'] : 0;
 $format = isset($_REQUEST['format']) ? $_REQUEST['format'] : 'rss2';
 
-$bugRepository = new BugRepository($dbh);
+$bugRepository = $container->get(BugRepository::class);
 $bug = $bugRepository->findOneById($bug_id);
 
 if (!$bug) {
@@ -29,7 +29,7 @@ if ($bug['private'] == 'Y') {
        die('Access restricted');
 }
 
-$commentRepository = new CommentRepository($dbh);
+$commentRepository = $container->get(CommentRepository::class);
 $comments = $commentRepository->findByBugId($bug_id);
 
 if ($format == 'xml') {
diff --git a/www/stats.php b/www/stats.php
index 8cdd57a..243a118 100644
--- a/www/stats.php
+++ b/www/stats.php
@@ -43,7 +43,7 @@ if (!array_key_exists($sort_by, $titles)) {
 }
 
 $bug_type = $_GET['bug_type'] ?? 'All';
-$bugRepository = new BugRepository($dbh);
+$bugRepository = $container->get(BugRepository::class);
 
 foreach ($bugRepository->findAllByBugType($bug_type) as $row) {
        $pkg_tmp[$row['status']][$row['package_name']] = $row['quant'];
diff --git a/www/vote.php b/www/vote.php
index e5d6817..ab3b675 100644
--- a/www/vote.php
+++ b/www/vote.php
@@ -23,7 +23,7 @@ $reproduced = (int) $_POST['reproduced'];
 $samever = isset($_POST['samever']) ? (int) $_POST['samever'] : 0;
 $sameos = isset($_POST['sameos']) ? (int) $_POST['sameos'] : 0;
 
-if (!(new BugRepository($dbh))->exists($id)) {
+if (!$container->get(BugRepository::class)->exists($id)) {
        session_start();
 
        // Authenticate
@@ -64,7 +64,7 @@ $ip = ip2long(get_real_ip());
 // TODO: check if ip address has been banned. hopefully this will never need 
to be implemented.
 
 // Check whether the user has already voted on this bug.
-if (empty((new VoteRepository($dbh))->findOneByIdAndIp($id, $ip))) {
+if (empty($container->get(VoteRepository::class)->findOneByIdAndIp($id, $ip))) 
{
        // If the user vote isn't found, create one.
        $dbh->prepare("
                INSERT INTO bugdb_votes (bug, ip, score, reproduced, tried, 
sameos, samever)
-- 
PHP Webmaster List Mailing List (http://www.php.net/)
To unsubscribe, visit: http://www.php.net/unsub.php

Reply via email to