Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with
or
.
Download ZIP

Loading…

Basz feature/accepted model controller plugin #2615

Closed
wants to merge 11 commits into from

6 participants

@weierophinney weierophinney was assigned
@Freeaqingme
Collaborator

@weierophinney I incorporated @basz 's feedback. Up for review.

@Freeaqingme
Collaborator

Rebased!

@Freeaqingme
Collaborator

This plugin allows one to easily and safely define what View Model type should be used based on the accept header string.

<?php
public function foobarAction()
{
   $arr = array(
        'Zend\View\Model\JsonModel' => array(
            'application/json',
            'application/javascript'
        ),
        'Zend\View\Model\FeedModel' => array(
            'application/rss+xml',
            'application/atom+xml'
        ),
        'Zend\View\Model\ViewModel' => '*/*'
    );


    $this->request->getHeaders()->addHeader($header);
    $viewModel = $this->acceptantViewModelSelector($arr);
    //    $viewModel now is an instance of Zend\View\Model\FeedModel
}
?>

Generally this mapping array will be defined on a controller level, or even application wide level. But it does allow one to deviate from the defaults.

The need for this functionality arises because of multiple difficulties currently experienced with accept header handling and view model selection:

  • The default render strategy strategy kicks in only after the controller has dispatched. However, based on the accept header it may be required to perform different actions.
  • The current render strategy setup is not capable of handling accept headers like /.
  • Because render strategies are defined on an application-wide level, security issues arise, eg. #2410
.../Mvc/Controller/Plugin/AcceptantViewModelSelector.php
((11 lines not shown))
+namespace Zend\Mvc\Controller\Plugin;
+
+use Zend\Http\Request;
+use Zend\Http\Header\Accept\FieldValuePart\AbstractFieldValuePart;
+use Zend\Mvc\Controller\Plugin\AbstractPlugin;
+use Zend\View\Model\ModelInterface;
+use Zend\Mvc\InjectApplicationEventInterface;
+use Zend\Mvc\MvcEvent;
+use Zend\Mvc\Exception\InvalidArgumentException;
+
+/**
+ * @category Zend
+ * @package Zend_Mvc
+ * @subpackage Controller
+ */
+class AcceptantViewModelSelector extends AbstractPlugin
@akrabat Collaborator
akrabat added a note

I think this should be AcceptedViewModelSelector

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
@weierophinney

My only concern at this point is whether or not the ViewManager should allow registering strategies at initialization or not.

Removing that functionality is technically a BC break, but, as you and others have noted, it could also be a potential security issue.

I'll ask the security team their opinion. Otherwise, this is a great set of functionality, and I look forward to merging it and using it.

...ader/Accept/FieldValuePart/AbstractFieldValuePart.php
@@ -37,6 +43,28 @@ public function __construct($internalValues)
}
/**
+ * Set a Field Value Part this Field Value Part matched against.
+ *
+ * @param AbstractFieldValuePart $matchedPart
+ * return AbstractFieldValuePart provides fluent interface
@Maks3w Collaborator
Maks3w added a note

missed @ in front of return

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
...ader/Accept/FieldValuePart/AbstractFieldValuePart.php
@@ -37,6 +43,28 @@ public function __construct($internalValues)
}
/**
+ * Set a Field Value Part this Field Value Part matched against.
+ *
+ * @param AbstractFieldValuePart $matchedPart
+ * return AbstractFieldValuePart provides fluent interface
+ */
+ public function setMatchedAgainst(AbstractFieldValuePart $matchedPart)
@Maks3w Collaborator
Maks3w added a note

IMO setter name should match with field name

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
@Maks3w Maks3w commented on the diff
library/Zend/Http/Header/AbstractAccept.php
@@ -302,8 +302,10 @@ public function match($matchAgainst)
foreach ($this->getPrioritized() as $left) {
foreach ($matchAgainst as $right) {
if ($right->type == '*' || $left->type == '*') {
- if ($res = $this->matchAcceptParams($left, $right)) {
- return $res;
+ if ($this->matchAcceptParams($left, $right)) {
+ $left->setMatchedAgainst($right);
+
+ return $left;
@Maks3w Collaborator
Maks3w added a note

This doesn't match with the @return signature, should return bool|array not a AbstractFieldValuePart object.

@Freeaqingme Collaborator

Docblocks were wrong (and had been before). Instead I updated those.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
@Maks3w Maks3w commented on the diff
library/Zend/Http/Header/AbstractAccept.php
@@ -313,8 +315,10 @@ public function match($matchAgainst)
($left->format == $right->format ||
$right->format == '*' || $left->format == '*')))
{
- if ($res = $this->matchAcceptParams($left, $right)) {
- return $res;
+ if ($this->matchAcceptParams($left, $right)) {
+ $left->setMatchedAgainst($right);
+
+ return $left;
@Maks3w Collaborator
Maks3w added a note

same here

@Freeaqingme Collaborator

Same: Docblocks were wrong (and had been before). Instead I updated those.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
...ader/Accept/FieldValuePart/AbstractFieldValuePart.php
@@ -37,6 +43,28 @@ public function __construct($internalValues)
}
/**
+ * Set a Field Value Part this Field Value Part matched against.
+ *
+ * @param AbstractFieldValuePart $matchedPart
+ * return AbstractFieldValuePart provides fluent interface
+ */
+ public function setMatchedAgainst(AbstractFieldValuePart $matchedPart)
+ {
+ $this->matchedPart = $matchedPart;
+ return $this;
+ }
+
+ /**
+ * Get a Field Value Part this Field Value Part matched against.
+ *
+ * return AbstractFieldValuePart|null
@Maks3w Collaborator
Maks3w added a note

missed @

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
...Mvc/Controller/Plugin/AcceptableViewModelSelector.php
((47 lines not shown))
+ * Default array to match against.
+ *
+ * @var Array
+ */
+ protected $defaultMatchAgainst;
+
+ /**
+ *
+ * @var string Default ViewModel
+ */
+ protected $defaultViewModelName = 'Zend\View\Model\ViewModel';
+
+ /**
+ * Detects an appropriate viewmodel for request.
+ *
+ * @param array (optional) $matchAgainst The Array to match against
@Maks3w Collaborator
Maks3w added a note

phpdoc does not accept (optional) at this point

@param [name] [<description>]

@Maks3w Collaborator
Maks3w added a note

the same in the rest of the block

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
...Mvc/Controller/Plugin/AcceptableViewModelSelector.php
((48 lines not shown))
+ *
+ * @var Array
+ */
+ protected $defaultMatchAgainst;
+
+ /**
+ *
+ * @var string Default ViewModel
+ */
+ protected $defaultViewModelName = 'Zend\View\Model\ViewModel';
+
+ /**
+ * Detects an appropriate viewmodel for request.
+ *
+ * @param array (optional) $matchAgainst The Array to match against
+ * @param bool (optional)$returnDefault If no match is availble. Return default instead
@Maks3w Collaborator
Maks3w added a note

s/avilble/available/

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
...Mvc/Controller/Plugin/AcceptableViewModelSelector.php
((64 lines not shown))
+ * @param AbstractFieldValuePart|null (optional) $resultReference The object that was matched
+ * @throws InvalidArgumentException If the supplied and matched View Model could not be found
+ * @return ModelInterface|null
+ */
+ public function __invoke(
+ array $matchAgainst = null,
+ $returnDefault = true,
+ & $resultReference = null)
+ {
+ return $this->getViewModel($matchAgainst, $returnDefault, $resultReference);
+ }
+
+ /**
+ * Detects an appropriate viewmodel for request.
+ *
+ * @param array (optional) $matchAgainst The Array to match against
@Maks3w Collaborator
Maks3w added a note

The same observations about phpdoc syntax as above

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
...Mvc/Controller/Plugin/AcceptableViewModelSelector.php
((65 lines not shown))
+ * @throws InvalidArgumentException If the supplied and matched View Model could not be found
+ * @return ModelInterface|null
+ */
+ public function __invoke(
+ array $matchAgainst = null,
+ $returnDefault = true,
+ & $resultReference = null)
+ {
+ return $this->getViewModel($matchAgainst, $returnDefault, $resultReference);
+ }
+
+ /**
+ * Detects an appropriate viewmodel for request.
+ *
+ * @param array (optional) $matchAgainst The Array to match against
+ * @param bool (optional)$returnDefault If no match is availble. Return default instead
@Maks3w Collaborator
Maks3w added a note

same typo

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
...Mvc/Controller/Plugin/AcceptableViewModelSelector.php
((92 lines not shown))
+ if (!$name) {
+ return;
+ }
+
+ if (!class_exists($name)) {
+ throw new InvalidArgumentException('The supplied View Model could not be found');
+ }
+
+ return new $name();
+ }
+
+
+ /**
+ * Detects an appropriate viewmodel name for request.
+ *
+ * @param array (optional) $matchAgainst The Array to match against
@Maks3w Collaborator
Maks3w added a note

same observations and typos as above

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
...Mvc/Controller/Plugin/AcceptableViewModelSelector.php
((128 lines not shown))
+ /**
+ * Detects an appropriate viewmodel name for request.
+ *
+ * @param array (optional) $matchAgainst The Array to match against
+ * @return AbstractFieldValuePart|null The object that was matched
+ */
+ public function match(array $matchAgainst = null)
+ {
+ $request = $this->getRequest();
+ $headers = $request->getHeaders();
+
+ if ((!$matchAgainst && !$this->defaultMatchString) || !$headers->has('accept')) {
+ return null;
+ }
+
+ if (!$matchAgainst) {
@Maks3w Collaborator
Maks3w added a note

I suggest move this block before the above check and simplify the complexity of the conditional sentence

@weierophinney Owner

@Maks3w The previous conditional is more specific, however, and will end execution of the method faster when matched. It makes sense.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
...Mvc/Controller/Plugin/AcceptableViewModelSelector.php
((7 lines not shown))
+ * @license http://framework.zend.com/license/new-bsd New BSD License
+ * @package Zend_Mvc
+ */
+
+namespace Zend\Mvc\Controller\Plugin;
+
+use Zend\Http\Request;
+use Zend\Http\Header\Accept\FieldValuePart\AbstractFieldValuePart;
+use Zend\Mvc\Controller\Plugin\AbstractPlugin;
+use Zend\View\Model\ModelInterface;
+use Zend\Mvc\InjectApplicationEventInterface;
+use Zend\Mvc\MvcEvent;
+use Zend\Mvc\Exception\InvalidArgumentException;
+
+/**
+ * @category Zend
@Maks3w Collaborator
Maks3w added a note

If possible add a description about the purpose of the class

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
...Mvc/Controller/Plugin/AcceptableViewModelSelector.php
((192 lines not shown))
+ */
+ protected function getRequest()
+ {
+ if ($this->request) {
+ return $this->request;
+ }
+
+ $event = $this->getEvent();
+ $request = $event->getRequest();
+ if (!$request instanceof Request) {
+ throw new Exception\DomainException(
+ 'The event used does not contain a valid Request, but must.'
+ );
+ }
+
+ return $this->request = $request;
@Maks3w Collaborator
Maks3w added a note

This is not very usual.

@Freeaqingme Collaborator

@Maks3w What do you propose?

@Maks3w Collaborator
Maks3w added a note

The same as the lines 236 & 238.

To be clear. I don't say if it's a bad practice or not, there is no rules about this. My concerns are about to make code with a predictable style and widely supported along the time. What do I mean? I mean even being a valid syntax, the could have the possibility of not work with future releases of PHP, simply because is a weird syntax. Something similar like the other day where a contributor proposed !!$var instead (bool) $var

@weierophinney Owner

@Freeaqingme What @Maks3w is proposing is you do the assignment and then return:

$this->request = $request;
return $request;
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
...Controller/Plugin/AcceptableViewModelSelectorTest.php
((153 lines not shown))
+ $this->request->getHeaders()->addHeader($header);
+
+ $result = $plugin->match(array( 'Zend\View\Model\ViewModel' => '*/*'));
+ $this->assertInstanceOf(
+ 'Zend\Http\Header\Accept\FieldValuePart\AcceptFieldValuePart',
+ $result
+ );
+ }
+
+ public function testInvalidModel()
+ {
+ $arr = array('DoesNotExist' => 'text/xml');
+ $header = Accept::fromString('Accept: */*');
+ $this->request->getHeaders()->addHeader($header);
+
+ try {
@Maks3w Collaborator
Maks3w added a note

use $this->setExpectedException instead of try/catch block

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
...Controller/Plugin/AcceptableViewModelSelectorTest.php
((45 lines not shown))
+ 'application/json',
+ 'application/javascript'
+ ),
+ 'Zend\View\Model\FeedModel' => array(
+ 'application/rss+xml',
+ 'application/atom+xml'
+ ),
+ 'Zend\View\Model\ViewModel' => '*/*'
+ );
+
+ $plugin = $this->plugin;
+ $header = Accept::fromString('Accept: text/plain; q=0.5, text/html, text/xml; q=0, text/x-dvi; q=0.8, text/x-c');
+ $this->request->getHeaders()->addHeader($header);
+ $result = $plugin($arr);
+
+ $this->assertInstanceOf('Zend\View\Model\ViewModel', $result);
@Maks3w Collaborator
Maks3w added a note

It's not clear if return ViewModel due */* or because is the default value of defaultViewModelName

@weierophinney Owner

Agreed with @Maks3w -- @Freeaqingme -- I think you need to add some assertNotInstanceOf() clauses here to ensure you don't have a Json or Feed model, as those extend ViewModel, and it's clear you're expecting specifically ViewModel, not an extension.

@Freeaqingme Collaborator

Although yuu both mention different (yet equally important) points, I fixed them both.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
@Maks3w Maks3w commented on the diff
...Mvc/Controller/Plugin/AcceptableViewModelSelector.php
((36 lines not shown))
+ * @var Zend\Mvc\MvcEvent
+ */
+ protected $event;
+
+ /**
+ *
+ * @var Zend\Http\Request
+ */
+ protected $request;
+
+ /**
+ * Default array to match against.
+ *
+ * @var Array
+ */
+ protected $defaultMatchAgainst;
@Maks3w Collaborator
Maks3w added a note

What method set a value to this field?

@Freeaqingme Collaborator

As discussed, a getter and setter were added.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
...Mvc/Controller/Plugin/AcceptableViewModelSelector.php
((57 lines not shown))
+ protected $defaultViewModelName = 'Zend\View\Model\ViewModel';
+
+ /**
+ * Detects an appropriate viewmodel for request.
+ *
+ * @param array (optional) $matchAgainst The Array to match against
+ * @param bool (optional)$returnDefault If no match is availble. Return default instead
+ * @param AbstractFieldValuePart|null (optional) $resultReference The object that was matched
+ * @throws InvalidArgumentException If the supplied and matched View Model could not be found
+ * @return ModelInterface|null
+ */
+ public function __invoke(
+ array $matchAgainst = null,
+ $returnDefault = true,
+ & $resultReference = null)
+ {
@weierophinney Owner

CS dictates moving the closing paren to the same line as the opening brace in these situations, and only one level of indentation for the arguments:

public function __invoke(
    array $matchAgainst = null,
    $returnDefault = true,
    & $resultReference = null
) {
@Freeaqingme Collaborator

Fixed. Also in the rest of the class.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
@juriansluiman

Would it be possible to make some use cases a bit easier to work with? In my particular case, I have quite often support for a Json model when application/json or application/javascript is requested, all other types are simply handled the same way. In case JSON is requested, it will always be for q=1 (so at top-priority).

I could imagine a simple way like this:

class MyController
{
  public function fooAction()
  {
    $service = $this->getService();
    $result  = $service->doSomeWork();

    if ($this->accept()->match(array('application/json', 'application/javascript'))) {
      return new JsonModel($result);
    }

    return $this->redirect()->toRoute('foo/bar');
  }
}

I can create my own plugin using Request::match() myself, but it seems something like this could also be implemented in this AcceptableViewModelSelector?

@Maks3w
Collaborator

@basz @Freeaqingme Don't forget send a PR to zf2-documentation documenting the plugn!

@basz
@Freeaqingme
Collaborator

@juriansluiman We could indeed implement such functionality. However, it should be 100% configurable, and may need a little bit more thought put into it. Lets get this baby done first, then look what else we could do.

@Maks3w No worries, you're getting your docs! ;)

@weierophinney

@Freeaqingme As noted yesterday on IRC, we'll need to merge this against the master branch as well. The only way I see to do this is to merge a range of commits or cherry-pick. My question is: do you want to do this, or are you okay with somebody else doing it? Both will rewrite history, so the question is if you want that rewrite to happen in your own branch, or in mine.

@weierophinney weierophinney referenced this pull request from a commit in weierophinney/zf2
@weierophinney weierophinney [#2410][#2615] Update readme
- Added security note about change to JsonStrategy and FeedStrategy, and
  also detail AcceptableViewModelSelector usage
b8b2ddc
@weierophinney weierophinney referenced this pull request from a commit in weierophinney/zf2
@weierophinney weierophinney [#2410][#2615] s/model/viewModel/
- per @Akrabat
07f20f8
@weierophinney weierophinney referenced this pull request from a commit in weierophinney/zf2
@weierophinney weierophinney [#2615] Test shuffling
- Split testInvokeWithoutDefaults() into two separate tests, for the
  separate behaviors it tested
  (testInvokeWithoutDefaultsReturnsNullWhenNoMatchesOccur() and
  testInvokeReturnsFieldValuePartOnMatchWhenReferenceProvided())
- Renamed testInvoke__2() to
  testHonorsAcceptPrecedenceAndPriorityWhenInvoked()
b1f0fbb
@weierophinney weierophinney referenced this pull request from a commit
@weierophinney weierophinney Merge branch 'hotfix/2615' into develop
Forward port #2615 and #2410

Conflicts:
	library/Zend/Mvc/Controller/PluginManager.php
8dae483
@weierophinney weierophinney closed this pull request from a commit
@weierophinney weierophinney Merge branch 'hotfix/2615'
Introduce AcceptableViewModelSelector, and remove Accept matching from
JsonStrategy and FeedStrategy

Close #2615
Close #2410
18bce16
@ghost Unknown referenced this pull request from a commit
@weierophinney weierophinney [#2410][#2615] Update readme
- Added security note about change to JsonStrategy and FeedStrategy, and
  also detail AcceptableViewModelSelector usage
9cae7be
@ghost Unknown referenced this pull request from a commit
@weierophinney weierophinney [#2410][#2615] s/model/viewModel/
- per @Akrabat
691b28a
@ghost Unknown referenced this pull request from a commit
@weierophinney weierophinney [#2615] Test shuffling
- Split testInvokeWithoutDefaults() into two separate tests, for the
  separate behaviors it tested
  (testInvokeWithoutDefaultsReturnsNullWhenNoMatchesOccur() and
  testInvokeReturnsFieldValuePartOnMatchWhenReferenceProvided())
- Renamed testInvoke__2() to
  testHonorsAcceptPrecedenceAndPriorityWhenInvoked()
4ea6b7b
@ghost Unknown referenced this pull request from a commit
@weierophinney weierophinney Merge branch 'hotfix/2615'
Introduce AcceptableViewModelSelector, and remove Accept matching from
JsonStrategy and FeedStrategy

Close #2615
Close #2410
163ce36
@ghost Unknown referenced this pull request from a commit
@weierophinney weierophinney Merge branch 'hotfix/2615' into develop
Forward port #2615 and #2410

Conflicts:
	library/Zend/Mvc/Controller/PluginManager.php
582fcb3
@weierophinney weierophinney referenced this pull request from a commit
Commit has since been removed from the repository and is no longer available.
@weierophinney weierophinney referenced this pull request from a commit
Commit has since been removed from the repository and is no longer available.
@weierophinney weierophinney referenced this pull request from a commit in zendframework/zend-http
@weierophinney weierophinney Merge branch 'hotfix/2615'
Introduce AcceptableViewModelSelector, and remove Accept matching from
JsonStrategy and FeedStrategy

Close zendframework/zf2#2615
Close zendframework/zf2#2410
1e41fbd
@weierophinney weierophinney referenced this pull request from a commit in zendframework/zend-http
@weierophinney weierophinney Merge branch 'hotfix/2615' into develop
Forward port zendframework/zf2#2615 and zendframework/zf2#2410

Conflicts:
	library/Zend/Mvc/Controller/PluginManager.php
ec1f916
@weierophinney weierophinney referenced this pull request from a commit in zendframework/zend-view
@weierophinney weierophinney Merge branch 'hotfix/2615'
Introduce AcceptableViewModelSelector, and remove Accept matching from
JsonStrategy and FeedStrategy

Close zendframework/zf2#2615
Close zendframework/zf2#2410
0ebadc5
@weierophinney weierophinney referenced this pull request from a commit in zendframework/zend-view
@weierophinney weierophinney Merge branch 'hotfix/2615' into develop
Forward port zendframework/zf2#2615 and zendframework/zf2#2410

Conflicts:
	library/Zend/Mvc/Controller/PluginManager.php
8ba0094
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
This page is out of date. Refresh to see the latest.
View
14 library/Zend/Http/Header/AbstractAccept.php
@@ -291,7 +291,7 @@ protected function hasType($matchAgainst)
* Match a media string against this header
*
* @param array|string $matchAgainst
- * @return array|boolean The matched value or false
+ * @return AcceptFieldValuePart|boolean The matched value or false
*/
public function match($matchAgainst)
{
@@ -302,8 +302,10 @@ public function match($matchAgainst)
foreach ($this->getPrioritized() as $left) {
foreach ($matchAgainst as $right) {
if ($right->type == '*' || $left->type == '*') {
- if ($res = $this->matchAcceptParams($left, $right)) {
- return $res;
+ if ($this->matchAcceptParams($left, $right)) {
+ $left->setMatchedAgainst($right);
+
+ return $left;
@Maks3w Collaborator
Maks3w added a note

This doesn't match with the @return signature, should return bool|array not a AbstractFieldValuePart object.

@Freeaqingme Collaborator

Docblocks were wrong (and had been before). Instead I updated those.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
}
}
@@ -313,8 +315,10 @@ public function match($matchAgainst)
($left->format == $right->format ||
$right->format == '*' || $left->format == '*')))
{
- if ($res = $this->matchAcceptParams($left, $right)) {
- return $res;
+ if ($this->matchAcceptParams($left, $right)) {
+ $left->setMatchedAgainst($right);
+
+ return $left;
@Maks3w Collaborator
Maks3w added a note

same here

@Freeaqingme Collaborator

Same: Docblocks were wrong (and had been before). Instead I updated those.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
}
}
}
View
28 library/Zend/Http/Header/Accept/FieldValuePart/AbstractFieldValuePart.php
@@ -28,6 +28,12 @@
private $internalValues;
/**
+ * A Field Value Part this Field Value Part matched against.
+ * @var AbstractFieldValuePart
+ */
+ protected $matchedAgainst;
+
+ /**
*
* @param object $internalValues
*/
@@ -37,6 +43,28 @@ public function __construct($internalValues)
}
/**
+ * Set a Field Value Part this Field Value Part matched against.
+ *
+ * @param AbstractFieldValuePart $matchedPart
+ * @return AbstractFieldValuePart provides fluent interface
+ */
+ public function setMatchedAgainst(AbstractFieldValuePart $matchedAgainst)
+ {
+ $this->matchedAgainst = $matchedAgainst;
+ return $this;
+ }
+
+ /**
+ * Get a Field Value Part this Field Value Part matched against.
+ *
+ * @return AbstractFieldValuePart|null
+ */
+ public function getMatchedAgainst()
+ {
+ return $this->matchedAgainst;
+ }
+
+ /**
*
* @return object
*/
View
288 library/Zend/Mvc/Controller/Plugin/AcceptableViewModelSelector.php
@@ -0,0 +1,288 @@
+<?php
+/**
+ * Zend Framework (http://framework.zend.com/)
+ *
+ * @link http://github.com/zendframework/zf2 for the canonical source repository
+ * @copyright Copyright (c) 2005-2012 Zend Technologies USA Inc. (http://www.zend.com)
+ * @license http://framework.zend.com/license/new-bsd New BSD License
+ * @package Zend_Mvc
+ */
+
+namespace Zend\Mvc\Controller\Plugin;
+
+use Zend\Http\Request;
+use Zend\Http\Header\Accept\FieldValuePart\AbstractFieldValuePart;
+use Zend\Mvc\Controller\Plugin\AbstractPlugin;
+use Zend\View\Model\ModelInterface;
+use Zend\Mvc\InjectApplicationEventInterface;
+use Zend\Mvc\MvcEvent;
+use Zend\Mvc\Exception\InvalidArgumentException;
+use Zend\Mvc\Exception\DomainException;
+
+
+/**
+ * Controller Plugin to assist in selecting an appropriate View Model type based on the
+ * User Agent's accept header.
+ *
+ * @category Zend
+ * @package Zend_Mvc
+ * @subpackage Controller
+ */
+class AcceptableViewModelSelector extends AbstractPlugin
+{
+ /**
+ *
+ * @var string the Key to inject the name of a viewmodel with in an Accept Header
+ */
+ const INJECT_VIEWMODEL_NAME = '_internalViewModel';
+
+ /**
+ *
+ * @var \Zend\Mvc\MvcEvent
+ */
+ protected $event;
+
+ /**
+ *
+ * @var \Zend\Http\Request
+ */
+ protected $request;
+
+ /**
+ * Default array to match against.
+ *
+ * @var Array
+ */
+ protected $defaultMatchAgainst;
@Maks3w Collaborator
Maks3w added a note

What method set a value to this field?

@Freeaqingme Collaborator

As discussed, a getter and setter were added.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
+
+ /**
+ *
+ * @var string Default ViewModel
+ */
+ protected $defaultViewModelName = 'Zend\View\Model\ViewModel';
+
+ /**
+ * Detects an appropriate viewmodel for request.
+ *
+ * @param array $matchAgainst (optional) The Array to match against
+ * @param bool $returnDefault (optional) If no match is available. Return default instead
+ * @param AbstractFieldValuePart|null $resultReference (optional) The object that was matched
+ * @throws InvalidArgumentException If the supplied and matched View Model could not be found
+ * @return ModelInterface|null
+ */
+ public function __invoke(
+ array $matchAgainst = null,
+ $returnDefault = true,
+ & $resultReference = null
+ ) {
+ return $this->getViewModel($matchAgainst, $returnDefault, $resultReference);
+ }
+
+ /**
+ * Detects an appropriate viewmodel for request.
+ *
+ * @param array $matchAgainst (optional) The Array to match against
+ * @param bool $returnDefault (optional) If no match is available. Return default instead
+ * @param AbstractFieldValuePart|null $resultReference (optional) The object that was matched
+ * @throws InvalidArgumentException If the supplied and matched View Model could not be found
+ * @return ModelInterface|null
+ */
+ public function getViewModel(
+ array $matchAgainst = null,
+ $returnDefault = true,
+ & $resultReference = null
+ ) {
+ $name = $this->getViewModelName($matchAgainst, $returnDefault, $resultReference);
+
+ if (!$name) {
+ return;
+ }
+
+ if (!class_exists($name)) {
+ throw new InvalidArgumentException('The supplied View Model could not be found');
+ }
+
+ return new $name();
+ }
+
+ /**
+ * Detects an appropriate viewmodel name for request.
+ *
+ * @param array $matchAgainst (optional) The Array to match against
+ * @param bool $returnDefault (optional) If no match is available. Return default instead
+ * @param AbstractFieldValuePart|null $resultReference (optional) The object that was matched.
+ * @return ModelInterface|null Returns null if $returnDefault = false and no match could be made
+ */
+ public function getViewModelName(
+ array $matchAgainst = null,
+ $returnDefault = true,
+ & $resultReference = null
+ ) {
+ $res = $this->match($matchAgainst);
+ if ($res) {
+ $resultReference = $res;
+ return $this->extractViewModelName($res);
+ }
+
+ if ($returnDefault) {
+ return $this->defaultViewModelName;
+ }
+ }
+
+ /**
+ * Detects an appropriate viewmodel name for request.
+ *
+ * @param array $matchAgainst (optional) The Array to match against
+ * @return AbstractFieldValuePart|null The object that was matched
+ */
+ public function match(array $matchAgainst = null)
+ {
+ $request = $this->getRequest();
+ $headers = $request->getHeaders();
+
+ if ((!$matchAgainst && !$this->defaultMatchAgainst) || !$headers->has('accept')) {
+ return null;
+ }
+
+ if (!$matchAgainst) {
+ $matchAgainst = $this->defaultMatchAgainst;
+ }
+
+ $matchAgainstString = '';
+ foreach ($matchAgainst as $modelName => $modelStrings) {
+ foreach ((array) $modelStrings as $modelString) {
+ $matchAgainstString .= $this->injectViewModelName($modelString, $modelName);
+ }
+ }
+
+ /** @var $accept \Zend\Http\Header\Accept */
+ $accept = $headers->get('Accept');
+ if (($res = $accept->match($matchAgainstString)) === false) {
+ return null;
+ }
+
+ return $res;
+ }
+
+ /**
+ * Set the default View Model (name) to return if no match could be made
+ * @param string $defaultViewModelName The default View Model name
+ * @return AcceptableViewModelSelector provides fluent interface
+ */
+ public function setDefaultViewModelName($defaultViewModelName)
+ {
+ $this->defaultViewModelName = (string) $defaultViewModelName;
+ return $this;
+ }
+
+ /**
+ * Set the default View Model (name) to return if no match could be made
+ * @return string
+ */
+ public function getDefaultViewModelName()
+ {
+ return $this->defaultViewModelName;
+ }
+
+ /**
+ * Set the default Accept Types and View Model combinations to match against if none are specified.
+ *
+ * @param array $matchAgainst (optional) The Array to match against
+ * @return AcceptableViewModelSelector provides fluent interface
+ */
+ public function setDefaultMatchAgainst(array $matchAgainst = null)
+ {
+ $this->defaultMatchAgainst = $matchAgainst;
+ return $this;
+ }
+
+ /**
+ * Get the default Accept Types and View Model combinations to match against if none are specified.
+ *
+ * @return array|null
+ */
+ public function getDefaultMatchAgainst()
+ {
+ return $this->defaultMatchAgainst;
+ }
+
+ /**
+ * Inject the viewmodel name into the accept header string
+ *
+ * @param string $modelAcceptString
+ * @param string $modelName
+ * @return string
+ */
+ protected function injectViewModelName($modelAcceptString, $modelName)
+ {
+ $modelName = str_replace('\\', '|', $modelName);
+ return $modelAcceptString . '; ' . self::INJECT_VIEWMODEL_NAME . '="' . $modelName . '", ';
+ }
+
+ /**
+ * Extract the viewmodel name from a match
+ * @param AbstractFieldValuePart $res
+ * @return string
+ */
+ protected function extractViewModelName(AbstractFieldValuePart $res)
+ {
+ $modelName = $res->getMatchedAgainst()->params[self::INJECT_VIEWMODEL_NAME];
+ return str_replace('|', '\\', $modelName);
+ }
+
+ /**
+ * Get the request
+ *
+ * @return Request
+ * @throws DomainException if unable to find request
+ */
+ protected function getRequest()
+ {
+ if ($this->request) {
+ return $this->request;
+ }
+
+ $event = $this->getEvent();
+ $request = $event->getRequest();
+ if (!$request instanceof Request) {
+ throw new DomainException(
+ 'The event used does not contain a valid Request, but must.'
+ );
+ }
+
+ $this->request = $request;
+ return $request;
+ }
+
+ /**
+ * Get the event
+ *
+ * @return MvcEvent
+ * @throws DomainException if unable to find event
+ */
+ protected function getEvent()
+ {
+ if ($this->event) {
+ return $this->event;
+ }
+
+ $controller = $this->getController();
+ if (!$controller instanceof InjectApplicationEventInterface) {
+ throw new DomainException(
+ 'A controller that implements InjectApplicationEventInterface '
+ . 'is required to use ' . __CLASS__
+ );
+ }
+
+ $event = $controller->getEvent();
+ if (!$event instanceof MvcEvent) {
+ $params = $event->getParams();
+ $event = new MvcEvent();
+ $event->setParams($params);
+ }
+ $this->event = $event;
+
+ return $this->event;
+ }
+
+}
View
17 library/Zend/Mvc/Controller/PluginManager.php
@@ -33,14 +33,15 @@ class PluginManager extends AbstractPluginManager
* @var array
*/
protected $invokableClasses = array(
- 'flashmessenger' => 'Zend\Mvc\Controller\Plugin\FlashMessenger',
- 'forward' => 'Zend\Mvc\Controller\Plugin\Forward',
- 'layout' => 'Zend\Mvc\Controller\Plugin\Layout',
- 'params' => 'Zend\Mvc\Controller\Plugin\Params',
- 'postredirectget' => 'Zend\Mvc\Controller\Plugin\PostRedirectGet',
- 'filepostredirectget' => 'Zend\Mvc\Controller\Plugin\FilePostRedirectGet',
- 'redirect' => 'Zend\Mvc\Controller\Plugin\Redirect',
- 'url' => 'Zend\Mvc\Controller\Plugin\Url',
+ 'acceptableviewmodelselector' => 'Zend\Mvc\Controller\Plugin\AcceptableViewModelSelector',
+ 'flashmessenger' => 'Zend\Mvc\Controller\Plugin\FlashMessenger',
+ 'forward' => 'Zend\Mvc\Controller\Plugin\Forward',
+ 'layout' => 'Zend\Mvc\Controller\Plugin\Layout',
+ 'params' => 'Zend\Mvc\Controller\Plugin\Params',
+ 'postredirectget' => 'Zend\Mvc\Controller\Plugin\PostRedirectGet',
+ 'filepostredirectget' => 'Zend\Mvc\Controller\Plugin\FilePostRedirectGet',
+ 'redirect' => 'Zend\Mvc\Controller\Plugin\Redirect',
+ 'url' => 'Zend\Mvc\Controller\Plugin\Url',
);
/**
View
32 tests/ZendTest/Http/Header/AcceptTest.php
@@ -193,6 +193,38 @@ public function testParsingAndAssemblingQuotedStrings()
}
+ public function testMatchReturnsMatchedAgainstObject()
+ {
+ $acceptStr = 'Accept: text/html;q=1; version=23; level=5, text/json;level=1,' .
+ 'text/xml;level=2;q=0.4';
+ $acceptHeader = Accept::fromString($acceptStr);
+
+ $res = $acceptHeader->match('text/html; _randomValue=foobar');
+ $this->assertInstanceOf(
+ 'Zend\Http\Header\Accept\FieldValuePart\AbstractFieldValuePart',
+ $res->getMatchedAgainst()
+ );
+ $this->assertEquals(
+ 'foobar',
+ $res->getMatchedAgainst()->getParams()->_randomValue
+ );
+
+ $acceptStr = 'Accept: */*; ';
+ $acceptHeader = Accept::fromString($acceptStr);
+
+ $res = $acceptHeader->match('text/html; _foo=bar');
+ $this->assertInstanceOf(
+ 'Zend\Http\Header\Accept\FieldValuePart\AbstractFieldValuePart',
+ $res->getMatchedAgainst()
+ );
+
+ $this->assertEquals(
+ 'bar',
+ $res->getMatchedAgainst()->getParams()->_foo
+ );
+ }
+
+
public function testVersioning()
{
$acceptStr = 'Accept: text/html;q=1; version=23; level=5, text/json;level=1,' .
View
200 tests/ZendTest/Mvc/Controller/Plugin/AcceptableViewModelSelectorTest.php
@@ -0,0 +1,200 @@
+<?php
+/**
+ * Zend Framework (http://framework.zend.com/)
+ *
+ * @link http://github.com/zendframework/zf2 for the canonical source repository
+ * @copyright Copyright (c) 2005-2012 Zend Technologies USA Inc. (http://www.zend.com)
+ * @license http://framework.zend.com/license/new-bsd New BSD License
+ * @package Zend_Mvc
+ */
+
+namespace ZendTest\Mvc\Controller\Plugin;
+
+use Zend\Mvc\Controller\Plugin\AcceptableViewModelSelector;
+use Zend\Http\Request;
+use Zend\Mvc\MvcEvent;
+use Zend\Http\Header\Accept;
+use ZendTest\Mvc\Controller\TestAsset\SampleController;
+
+/**
+ * @category Zend
+ * @package Zend_Mvc
+ * @subpackage UnitTests
+ */
+class AcceptableViewModelSelectorTest extends \PHPUnit_Framework_TestCase
+{
+ public function setUp()
+ {
+ $this->request = new Request();
+
+ $event = new MvcEvent();
+ $event->setRequest($this->request);
+ $this->event = $event;
+
+ $this->controller = new SampleController();
+ $this->controller->setEvent($event);
+
+ /** @var Zend\Mvc\Controller\Plugin\AcceptableViewModelSelector */
+ $this->plugin = $this->controller->plugin('acceptableViewModelSelector');
+ }
+
+ public function testInvoke()
+ {
+ $arr = array(
+ 'Zend\View\Model\JsonModel' => array(
+ 'application/json',
+ 'application/javascript'
+ ),
+ 'Zend\View\Model\FeedModel' => array(
+ 'application/rss+xml',
+ 'application/atom+xml'
+ ),
+ 'Zend\View\Model\ViewModel' => '*/*'
+ );
+
+ $header = Accept::fromString('Accept: text/plain; q=0.5, text/html, text/xml; q=0, text/x-dvi; q=0.8, text/x-c');
+ $this->request->getHeaders()->addHeader($header);
+ $plugin = $this->plugin;
+ $plugin->setDefaultViewModelName('Zend\View\Model\FeedModel');
+ $result = $plugin($arr);
+
+ $this->assertInstanceOf('Zend\View\Model\ViewModel', $result);
+ $this->assertNotInstanceOf('Zend\View\Model\FeedModel', $result); // Ensure the default wasn't selected
+ $this->assertNotInstanceOf('Zend\View\Model\JsonModel', $result);
+ }
+
+ public function testDefaultViewModelName()
+ {
+ $arr = array(
+ 'Zend\View\Model\JsonModel' => array(
+ 'application/json',
+ 'application/javascript'
+ ),
+ 'Zend\View\Model\FeedModel' => array(
+ 'application/rss+xml',
+ 'application/atom+xml'
+ ),
+ );
+
+ $header = Accept::fromString('Accept: text/plain');
+ $this->request->getHeaders()->addHeader($header);
+ $plugin = $this->plugin;
+ $result = $plugin->getViewModelName($arr);
+
+ $this->assertEquals('Zend\View\Model\ViewModel', $result); // Default Default View Model Name
+
+ $plugin->setDefaultViewModelName('Zend\View\Model\FeedModel');
+ $this->assertEquals($plugin->getDefaultViewModelName(), 'Zend\View\Model\FeedModel'); // Test getter along the way
+ $this->assertInstanceOf('Zend\View\Model\FeedModel', $plugin($arr));
+ }
+
+
+
+ public function testInvoke_2()
+ {
+ $arr = array(
+ 'Zend\View\Model\JsonModel' => array(
+ 'application/json',
+ 'application/javascript'
+ ),
+ 'Zend\View\Model\FeedModel' => array(
+ 'application/rss+xml',
+ 'application/atom+xml'
+ ),
+ 'Zend\View\Model\ViewModel' => '*/*'
+ );
+
+ $plugin = $this->plugin;
+ $header = Accept::fromString('Accept: application/rss+xml; version=0.2');
+ $this->request->getHeaders()->addHeader($header);
+ $result = $plugin($arr);
+
+ $this->assertInstanceOf('Zend\View\Model\FeedModel', $result);
+ }
+
+
+ public function testInvokeWithoutDefaults()
+ {
+ $arr = array(
+ 'Zend\View\Model\JsonModel' => array(
+ 'application/json',
+ 'application/javascript'
+ ),
+ 'Zend\View\Model\FeedModel' => array(
+ 'application/rss+xml',
+ 'application/atom+xml'
+ ),
+ );
+
+ $plugin = $this->plugin;
+ $header = Accept::fromString('Accept: text/html; version=0.2');
+ $this->request->getHeaders()->addHeader($header);
+
+ $result = $plugin($arr, false);
+ $this->assertNull($result);
+
+ $ref = null;
+ $result = $plugin(array( 'Zend\View\Model\ViewModel' => '*/*'), false, $ref);
+ $this->assertInstanceOf('Zend\View\Model\ViewModel', $result);
+ $this->assertNotInstanceOf('Zend\View\Model\JsonModel', $result);
+ $this->assertNotInstanceOf('Zend\View\Model\FeedModel', $result);
+ $this->assertInstanceOf('Zend\Http\Header\Accept\FieldValuePart\AcceptFieldValuePart', $ref);
+ }
+
+ public function testGetViewModelNameWithoutDefaults()
+ {
+ $arr = array(
+ 'Zend\View\Model\JsonModel' => array(
+ 'application/json',
+ 'application/javascript'
+ ),
+ 'Zend\View\Model\FeedModel' => array(
+ 'application/rss+xml',
+ 'application/atom+xml'
+ ),
+ );
+
+ $plugin = $this->plugin;
+ $header = Accept::fromString('Accept: text/html; version=0.2');
+ $this->request->getHeaders()->addHeader($header);
+
+ $result = $plugin->getViewModelName($arr, false);
+ $this->assertNull($result);
+
+ $ref = null;
+ $result = $plugin->getViewModelName(array( 'Zend\View\Model\ViewModel' => '*/*'), false, $ref);
+ $this->assertEquals('Zend\View\Model\ViewModel', $result);
+ $this->assertInstanceOf('Zend\Http\Header\Accept\FieldValuePart\AcceptFieldValuePart', $ref);
+ }
+
+ public function testMatch()
+ {
+
+
+ $plugin = $this->plugin;
+ $header = Accept::fromString('Accept: text/html; version=0.2');
+ $this->request->getHeaders()->addHeader($header);
+
+ $arr = array( 'Zend\View\Model\ViewModel' => '*/*');
+ $plugin->setDefaultMatchAgainst($arr);
+ $this->assertEquals($plugin->getDefaultMatchAgainst(), $arr);
+ $result = $plugin->match();
+ $this->assertInstanceOf(
+ 'Zend\Http\Header\Accept\FieldValuePart\AcceptFieldValuePart',
+ $result
+ );
+ $this->assertEquals($plugin->getDefaultMatchAgainst(), $arr);
+
+ }
+
+ public function testInvalidModel()
+ {
+ $arr = array('DoesNotExist' => 'text/xml');
+ $header = Accept::fromString('Accept: */*');
+ $this->request->getHeaders()->addHeader($header);
+
+ $this->setExpectedException('\Zend\Mvc\Exception\InvalidArgumentException');
+
+ $this->plugin->getViewModel($arr);
+ }
+}
Something went wrong with that request. Please try again.