Browse Source

Merge pull request #11006 from cakephp/form-context-factory

Add ContextFactory class.
Mark Story 8 years ago
parent
commit
699e27ed71

+ 155 - 0
src/View/Form/ContextFactory.php

@@ -0,0 +1,155 @@
+<?php
+/**
+ * CakePHP(tm) : Rapid Development Framework (https://cakephp.org)
+ * Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
+ *
+ * Licensed under The MIT License
+ * For full copyright and license information, please see the LICENSE.txt
+ * Redistributions of files must retain the above copyright notice.
+ *
+ * @copyright     Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
+ * @link          https://cakephp.org CakePHP(tm) Project
+ * @since         3.5.0
+ * @license       https://opensource.org/licenses/mit-license.php MIT License
+ */
+namespace Cake\View\Form;
+
+use Cake\Collection\Collection;
+use Cake\Datasource\EntityInterface;
+use Cake\Form\Form;
+use Cake\Http\ServerRequest;
+use RuntimeException;
+use Traversable;
+
+/**
+ * Factory for getting form context instance based on provided data.
+ */
+class ContextFactory
+{
+    /**
+     * Context providers.
+     *
+     * @var array
+     */
+    protected $providers = [];
+
+    /**
+     * Constructor.
+     *
+     * @param array $providers Array of provider callables. Each element should
+     *   be of form `['type' => 'a-string', 'callable' => ..]`
+     */
+    public function __construct(array $providers = [])
+    {
+        foreach ($providers as $provider) {
+            $this->addProvider($provider['type'], $provider['callable']);
+        }
+    }
+
+    /**
+     * Create factory instance with providers "array", "form" and "orm".
+     *
+     * @param array $providers Array of provider callables. Each element should
+     *   be of form `['type' => 'a-string', 'callable' => ..]`
+     * @return \Cake\View\Form\ContextFactory
+     */
+    public static function createWithDefaults(array $providers = [])
+    {
+        $providers = [
+            [
+                'type' => 'orm',
+                'callable' => function ($request, $data) {
+                    if (is_array($data['entity']) || $data['entity'] instanceof Traversable) {
+                        $pass = (new Collection($data['entity']))->first() !== null;
+                        if ($pass) {
+                            return new EntityContext($request, $data);
+                        }
+                    }
+                    if ($data['entity'] instanceof EntityInterface) {
+                        return new EntityContext($request, $data);
+                    }
+                    if (is_array($data['entity']) && empty($data['entity']['schema'])) {
+                        return new EntityContext($request, $data);
+                    }
+                }
+            ],
+            [
+                'type' => 'array',
+                'callable' => function ($request, $data) {
+                    if (is_array($data['entity']) && isset($data['entity']['schema'])) {
+                        return new ArrayContext($request, $data['entity']);
+                    }
+                }
+            ],
+            [
+                'type' => 'form',
+                'callable' => function ($request, $data) {
+                    if ($data['entity'] instanceof Form) {
+                        return new FormContext($request, $data);
+                    }
+                }
+            ],
+        ] + $providers;
+
+        return new static($providers);
+    }
+
+    /**
+     * Add a new context type.
+     *
+     * Form context types allow FormHelper to interact with
+     * data providers that come from outside CakePHP. For example
+     * if you wanted to use an alternative ORM like Doctrine you could
+     * create and connect a new context class to allow FormHelper to
+     * read metadata from doctrine.
+     *
+     * @param string $type The type of context. This key
+     *   can be used to overwrite existing providers.
+     * @param callable $check A callable that returns an object
+     *   when the form context is the correct type.
+     * @return $this
+     */
+    public function addProvider($type, callable $check)
+    {
+        $this->providers = [$type => ['type' => $type, 'callable' => $check]]
+            + $this->providers;
+
+        return $this;
+    }
+
+    /**
+     * Find the matching context for the data.
+     *
+     * If no type can be matched a NullContext will be returned.
+     *
+     * @param \Cake\Http\ServerRequest $request Request instance.
+     * @param array $data The data to get a context provider for.
+     * @return \Cake\View\Form\ContextInterface Context provider.
+     * @throws \RuntimeException when the context class does not implement the
+     *   ContextInterface.
+     */
+    public function get(ServerRequest $request, array $data = [])
+    {
+        $data += ['entity' => null];
+
+        foreach ($this->providers as $provider) {
+            $check = $provider['callable'];
+            $context = $check($request, $data);
+            if ($context) {
+                break;
+            }
+        }
+        if (!isset($context)) {
+            $context = new NullContext($request, $data);
+        }
+        if (!($context instanceof ContextInterface)) {
+            throw new RuntimeException(sprintf(
+                'Context providers must return object implementing %s. Got "%s" instead.',
+                ContextInterface::class,
+                is_object($context) ? get_class($context) : gettype($context)
+            ));
+        }
+
+        return $context;
+    }
+}

+ 18 - 60
src/View/Helper/FormHelper.php

@@ -14,19 +14,14 @@
  */
 namespace Cake\View\Helper;
 
-use Cake\Collection\Collection;
 use Cake\Core\Configure;
 use Cake\Core\Exception\Exception;
-use Cake\Datasource\EntityInterface;
 use Cake\Form\Form;
 use Cake\Routing\Router;
 use Cake\Utility\Hash;
 use Cake\Utility\Inflector;
-use Cake\View\Form\ArrayContext;
+use Cake\View\Form\ContextFactory;
 use Cake\View\Form\ContextInterface;
-use Cake\View\Form\EntityContext;
-use Cake\View\Form\FormContext;
-use Cake\View\Form\NullContext;
 use Cake\View\Helper;
 use Cake\View\StringTemplateTrait;
 use Cake\View\View;
@@ -206,12 +201,11 @@ class FormHelper extends Helper
     protected $_context;
 
     /**
-     * Context provider methods.
+     * Context factory.
      *
-     * @var array
-     * @see \Cake\View\Helper\FormHelper::addContextProvider()
+     * @var \Cake\View\Form\ContextFactory
      */
-    protected $_contextProviders = [];
+    protected $_contextFactory;
 
     /**
      * The action attribute value of the last created form.
@@ -253,7 +247,6 @@ class FormHelper extends Helper
         parent::__construct($View, $config);
 
         $this->widgetRegistry($registry, $widgets);
-        $this->_addDefaultContextProviders();
         $this->_idPrefix = $this->getConfig('idPrefix');
     }
 
@@ -279,38 +272,24 @@ class FormHelper extends Helper
     }
 
     /**
-     * Add the default suite of context providers provided by CakePHP.
+     * Set the context factory the helper will use.
      *
-     * @return void
+     * @param \Cake\View\Form\ContextFactory|null $instance The context factory instance to set.
+     * @param array $contexts An array of context providers.
+     * @return \Cake\View\Form\ContextFactory
      */
-    protected function _addDefaultContextProviders()
+    public function contextFactory(ContextFactory $instance = null, array $contexts = [])
     {
-        $this->addContextProvider('orm', function ($request, $data) {
-            if (is_array($data['entity']) || $data['entity'] instanceof Traversable) {
-                $pass = (new Collection($data['entity']))->first() !== null;
-                if ($pass) {
-                    return new EntityContext($request, $data);
-                }
-            }
-            if ($data['entity'] instanceof EntityInterface) {
-                return new EntityContext($request, $data);
-            }
-            if (is_array($data['entity']) && empty($data['entity']['schema'])) {
-                return new EntityContext($request, $data);
+        if ($instance === null) {
+            if ($this->_contextFactory === null) {
+                $this->_contextFactory = ContextFactory::createWithDefaults($contexts);
             }
-        });
 
-        $this->addContextProvider('form', function ($request, $data) {
-            if ($data['entity'] instanceof Form) {
-                return new FormContext($request, $data);
-            }
-        });
+            return $this->_contextFactory;
+        }
+        $this->_contextFactory = $instance;
 
-        $this->addContextProvider('array', function ($request, $data) {
-            if (is_array($data['entity']) && isset($data['entity']['schema'])) {
-                return new ArrayContext($request, $data['entity']);
-            }
-        });
+        return $this->_contextFactory;
     }
 
     /**
@@ -2687,12 +2666,7 @@ class FormHelper extends Helper
      */
     public function addContextProvider($type, callable $check)
     {
-        foreach ($this->_contextProviders as $i => $provider) {
-            if ($provider['type'] === $type) {
-                unset($this->_contextProviders[$i]);
-            }
-        }
-        array_unshift($this->_contextProviders, ['type' => $type, 'callable' => $check]);
+        $this->contextFactory()->addProvider($type, $check);
     }
 
     /**
@@ -2729,23 +2703,7 @@ class FormHelper extends Helper
         }
         $data += ['entity' => null];
 
-        foreach ($this->_contextProviders as $provider) {
-            $check = $provider['callable'];
-            $context = $check($this->request, $data);
-            if ($context) {
-                break;
-            }
-        }
-        if (!isset($context)) {
-            $context = new NullContext($this->request, $data);
-        }
-        if (!($context instanceof ContextInterface)) {
-            throw new RuntimeException(
-                'Context objects must implement Cake\View\Form\ContextInterface'
-            );
-        }
-
-        return $this->_context = $context;
+        return $this->_context = $this->contextFactory()->get($this->request, $data);
     }
 
     /**

+ 1 - 1
tests/TestCase/View/Helper/FormHelperTest.php

@@ -386,7 +386,7 @@ class FormHelperTest extends TestCase
      * Test adding an invalid context class.
      *
      * @expectedException \RuntimeException
-     * @expectedExceptionMessage Context objects must implement Cake\View\Form\ContextInterface
+     * @expectedExceptionMessage Context providers must return object implementing Cake\View\Form\ContextInterface. Got "stdClass" instead.
      * @return void
      */
     public function testAddContextProviderInvalid()