Commit:    d337e731ff8552e8bd3c3798bbb9a1104343c874
Author:    Peter Kokot <peterko...@gmail.com>         Wed, 5 Dec 2018 15:40:19 
+0100
Parents:   b4891d7bef11454c4024c8beb577a15cde24e0ae
Branches:  master

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

Log:
Add dual PSR-4 compatible classes autoloader

This patch is a workaround until Composer installation step can be used
in production bugs.php.net. Once Composer can be added to the deployment
step together with rsync this can be simplified and only Composer's
autoload.php will be used.

Changed paths:
  M  include/prepend.php
  A  src/Autoloader.php
  A  tests/AutoloaderTest.php
  M  www/bug-pwd-finder.php
  M  www/bug.php
  M  www/gh-pull-add.php
  M  www/patch-add.php
  M  www/report.php

diff --git a/include/prepend.php b/include/prepend.php
index 5a84714..3efa720 100644
--- a/include/prepend.php
+++ b/include/prepend.php
@@ -1,7 +1,18 @@
 <?php
 
+use App\Autoloader;
+
+// Dual PSR-4 compatible class autoloader. When Composer is not available, an
+// application specific replacement class is used. Once Composer can be added
+// to the deployment step with rsync this can be simplified and only Composer's
+// autoload.php will be used.
 if (file_exists(__DIR__.'/../vendor/autoload.php')) {
     require_once __DIR__.'/../vendor/autoload.php';
+} else {
+    require_once __DIR__.'/../src/Autoloader.php';
+
+    $loader = new Autoloader();
+    $loader->addNamespace('App\\', __DIR__.'/../src/');
 }
 
 $site = 'php';
diff --git a/src/Autoloader.php b/src/Autoloader.php
new file mode 100644
index 0000000..ae526e0
--- /dev/null
+++ b/src/Autoloader.php
@@ -0,0 +1,201 @@
+<?php
+
+namespace App;
+
+/**
+ * This is a PSR-4 autoloader based on the example implementation by the 
PHP-FIG
+ * at https://www.php-fig.org/psr/psr-4/. It includes an optional functionality
+ * of allowing multiple base directories for a single namespace prefix. A
+ * separate implementation besides the Composer's autoloader is done for cases
+ * when Composer is not available on the server environment such as production.
+ * It also provides loading non-PSR-4 compliant classes.
+ *
+ * Given a foo-bar package of classes in the file system at the following
+ * paths ...
+ *
+ *     /path/to/packages/foo-bar/
+ *         src/
+ *             Baz.php             # Foo\Bar\Baz
+ *             Qux/
+ *                 Quux.php        # Foo\Bar\Qux\Quux
+ *         tests/
+ *             BazTest.php         # Foo\Bar\BazTest
+ *             Qux/
+ *                 QuuxTest.php    # Foo\Bar\Qux\QuuxTest
+ *
+ * ... add the path to the class files for the \Foo\Bar\ namespace prefix
+ * as follows:
+ *
+ *      <?php
+ *      // Instantiate the loader to registers the SPL autoload
+ *      $loader = new App\Autoloader;
+ *
+ *      // Register the base directories for the namespace prefix
+ *      $loader->addNamespace('Foo\Bar', '/path/to/packages/foo-bar/src');
+ *      $loader->addNamespace('Foo\Bar', '/path/to/packages/foo-bar/tests');
+ *
+ * The following line would cause the autoloader to attempt to load the
+ * \Foo\Bar\Qux\Quux class from /path/to/packages/foo-bar/src/Qux/Quux.php:
+ *
+ *      <?php
+ *      new \Foo\Bar\Qux\Quux;
+ *
+ * The following line would cause the autoloader to attempt to load the
+ * \Foo\Bar\Qux\QuuxTest class from 
/path/to/packages/foo-bar/tests/Qux/QuuxTest.php:
+ *
+ *      <?php
+ *      new \Foo\Bar\Qux\QuuxTest;
+ */
+class Autoloader
+{
+    /**
+     * An associative array with namespace prefixes as keys and values of 
arrays
+     * of base directories for classes in that namespace.
+     */
+    protected $prefixes = [];
+
+    /**
+     * An associative array of classes as keys and their paths as values.
+     */
+    protected $classmap = [];
+
+    /**
+     * Class constructor that registers loader with a SPL autoloader stack.
+     */
+    public function __construct()
+    {
+        spl_autoload_register([$this, 'load']);
+    }
+
+    /**
+     * Adds a base directory for a namespace prefix.
+     *
+     * @param string $prefix The namespace prefix.
+     * @param string $baseDir A base directory for class files in the
+     * namespace.
+     * @param bool $prepend If true, prepend the base directory to the stack
+     * instead of appending it; this causes it to be searched first rather
+     * than last.
+     */
+    public function addNamespace($prefix, $baseDir, $prepend = false)
+    {
+        // normalize namespace prefix
+        $prefix = trim($prefix, '\\') . '\\';
+
+        // normalize the base directory with a trailing separator
+        $baseDir = rtrim($baseDir, DIRECTORY_SEPARATOR) . '/';
+
+        // initialize the namespace prefix array
+        if (isset($this->prefixes[$prefix]) === false) {
+            $this->prefixes[$prefix] = [];
+        }
+
+        // retain the base directory for the namespace prefix
+        if ($prepend) {
+            array_unshift($this->prefixes[$prefix], $baseDir);
+        } else {
+            array_push($this->prefixes[$prefix], $baseDir);
+        }
+    }
+
+    /**
+     * Add a classmap. Classmap is a simplistic imitation of the Composer's
+     * classmap autoloading.
+     */
+    public function addClassmap($class, $path)
+    {
+        $this->classmap[$class] = $path;
+    }
+
+    /**
+     * Loads the class file for a given class name.
+     *
+     * @param string $class The fully-qualified class name.
+     * @return mixed The mapped file name on success, or boolean false on
+     * failure.
+     */
+    public function load($class)
+    {
+        // the current namespace prefix
+        $prefix = $class;
+
+        // Work backwards through the namespace names of the fully-qualified
+        // class name to find a mapped file name
+        while (false !== $pos = strrpos($prefix, '\\')) {
+
+            // retain the trailing namespace separator in the prefix
+            $prefix = substr($class, 0, $pos + 1);
+
+            // the rest is the relative class name
+            $relativeClass = substr($class, $pos + 1);
+
+            // try to load a mapped file for the prefix and relative class
+            $mappedFile = $this->loadMappedFile($prefix, $relativeClass);
+            if ($mappedFile) {
+                return $mappedFile;
+            }
+
+            // Remove the trailing namespace separator for the next iteration
+            // of strrpos()
+            $prefix = rtrim($prefix, '\\');
+        }
+
+        // Check if file is maybe in classmap
+        if (!empty($this->classmap[$class])) {
+            return $this->requireFile($this->classmap[$class]) ? 
$this->classmap[$class] : false;
+        }
+
+        // Mapped file not found
+        return false;
+    }
+
+    /**
+     * Load the mapped file for a namespace prefix and relative class.
+     *
+     * @param string $prefix The namespace prefix.
+     * @param string $relativeClass The relative class name.
+     * @return mixed Boolean false if no mapped file can be loaded, or the
+     * name of the mapped file that was loaded.
+     */
+    protected function loadMappedFile($prefix, $relativeClass)
+    {
+        // are there any base directories for this namespace prefix?
+        if (isset($this->prefixes[$prefix]) === false) {
+            return false;
+        }
+
+        // Look through base directories for this namespace prefix
+        foreach ($this->prefixes[$prefix] as $baseDir) {
+            // replace the namespace prefix with the base directory,
+            // replace namespace separators with directory separators
+            // in the relative class name, append with .php
+            $file = $baseDir
+                  . str_replace('\\', '/', $relativeClass)
+                  . '.php';
+
+            // If the mapped file exists, require it
+            if ($this->requireFile($file)) {
+                return $file;
+            }
+        }
+
+        // Mapped file not found
+        return false;
+    }
+
+    /**
+     * If a file exists, require it from the file system.
+     *
+     * @param string $file The file to require.
+     * @return bool True if the file exists, false if not.
+     */
+    protected function requireFile($file)
+    {
+        if (file_exists($file)) {
+            require_once $file;
+            return true;
+        }
+
+        return false;
+    }
+}
diff --git a/tests/AutoloaderTest.php b/tests/AutoloaderTest.php
new file mode 100644
index 0000000..16ef816
--- /dev/null
+++ b/tests/AutoloaderTest.php
@@ -0,0 +1,99 @@
+<?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/www/bug-pwd-finder.php b/www/bug-pwd-finder.php
index 023597a..ffe8a38 100644
--- a/www/bug-pwd-finder.php
+++ b/www/bug-pwd-finder.php
@@ -4,16 +4,14 @@
 
 use App\Utils\Captcha;
 
-require_once __DIR__.'/../src/Utils/Captcha.php';
+// Obtain common includes
+require_once '../include/prepend.php';
 
 // Start session (for captcha!)
 session_start();
 
 $captcha = new Captcha();
 
-// Obtain common includes
-require_once '../include/prepend.php';
-
 $errors  = [];
 $success = false;
 $bug_id = isset($_REQUEST['id']) ? (int) $_REQUEST['id'] : 0;
diff --git a/www/bug.php b/www/bug.php
index d0bde4b..e2c6576 100644
--- a/www/bug.php
+++ b/www/bug.php
@@ -115,7 +115,6 @@ if ($edit == 1 && $is_trusted_developer && 
isset($_GET['delete_comment'])) {
 
 // captcha is not necessary if the user is logged in
 if (!$logged_in) {
-       require_once __DIR__.'/../src/Utils/Captcha.php';
        $captcha = new Captcha();
 }
 
diff --git a/www/gh-pull-add.php b/www/gh-pull-add.php
index f9bf6d6..93e147a 100644
--- a/www/gh-pull-add.php
+++ b/www/gh-pull-add.php
@@ -38,7 +38,6 @@ $is_trusted_developer = ($user_flags & BUGS_TRUSTED_DEV);
 
 // captcha is not necessary if the user is logged in
 if (!$logged_in) {
-       require_once __DIR__.'/../src/Utils/Captcha.php';
        $captcha = new Captcha();
 }
 
diff --git a/www/patch-add.php b/www/patch-add.php
index 6fd7035..0b1db80 100644
--- a/www/patch-add.php
+++ b/www/patch-add.php
@@ -38,7 +38,6 @@ $is_trusted_developer = ($user_flags & BUGS_TRUSTED_DEV);
 
 // captcha is not necessary if the user is logged in
 if (!$logged_in) {
-       require_once __DIR__.'/../src/Utils/Captcha.php';
        $captcha = new Captcha();
 }
 
diff --git a/www/report.php b/www/report.php
index 542bf87..c2a52f4 100644
--- a/www/report.php
+++ b/www/report.php
@@ -25,7 +25,6 @@ require "{$ROOT_DIR}/include/php_versions.php";
 
 // captcha is not necessary if the user is logged in
 if (!$logged_in) {
-       require_once __DIR__.'/../src/Utils/Captcha.php';
        $captcha = new Captcha();
 }
-- 
PHP Webmaster List Mailing List (http://www.php.net/)
To unsubscribe, visit: http://www.php.net/unsub.php

Reply via email to