Hello everybody, This a short draft that lacks many subjects about best practices with MvcTools. I intend to continue it little to little, as soon as persons dare disagreeing with me on IRC (joke, chill).
Please review the idea of adding such to the tutorial, the patch itself, and feel free to correct/commit it. Index: tutorial.txt =================================================================== --- tutorial.txt (revision 9133) +++ tutorial.txt (working copy) @@ -559,6 +559,159 @@ └── config.php +Best practices +`````````````` + +Exploit the most of MvcTools requires to understand the following concept: +it was first made to supply a standart design pattern to re-use code between +projects depending on eZ Components. +ezcMvcController illustrates this concept: it supplies a default simple +action dispatching logic. The only reason for that is to make it a little more +useful than an empty class. You will probably have to sub-class it and that's +not a problem at all. + +Controller design +----------------- + +The default action dispatching logic is implemented in createResult(). It is +perfectly fine to overload createResult() for example to implement one action +per class. + +On the other hand, if any code should be executed before or after the action +itself, it's OK to overload and decorate createResult() this way:: + + + /** + * Something is done before the action is executed. Something is done after + * the action is executed. + * + * @return void + * @todo factorize pre/post action code into filters. + */ + public function createResult() + { + doSomethingBefore(); + $result = parent::createResult(); + doSomethingAfter(); + + return $result; + } + +This is fine, as long as doSomethingBefore() and doSomethingAfter() is not +re-used in another controller. When this happens, it means that this custom +methods should be factorized in request or result filters. Such +factorization might be tedious and break backward compatibility: which requires +modifying tests: a bad idea in general. +This is a reason why you are strongly adviced to directly encapsulate such +processes in filters. +Another reason is that decoupling doSomethingBefore() from the controller will +allow testing smaller units. +Because filters are run in the configuration, the controller might depend on +request variables set by filters that were removed from the configuration +for some reason. To keep both the controller and programmer safe, an exception +should be thrown if a required variable is missing from the request, which will +help in debugging. + +Testing filters +--------------- + +Filters should be small unit respecting the same interface, making it fast to +code a test-case with PHPUnit @dataProvider, for example:: + + /** + * @dataProvider getFooRequestFixtures() + */ + public function testFooRequestFilter( $expectedRequest, $fixtureRequest, $testName ) + { + $filter = new yourFooFilter(); + $resultingRequest = $filter->filterRequest( $fixtureRequest ); + $this->assertEqual( $expectedRequest, $resultingRequest, + "$testName expected request should match actual expected request." ); + } + + protected $fixturePath = ''; + public function setUp() + { + $this->fixturePath = '/tests/foofilter'; + } + + /** + * Loops over $fixturePath and returns an array of array( $expected, $fixture ). + * + * If $fixturePath is set to /tests/foofilter, and if it had the following + * structure: + * - /tests + * - /foofilter + * - /testWithSomething + * - expected_request.php + * - fixture_request.php + * - [...] + * - /testWithSomethingElse + * - expected_request.php + * - fixture_request.php + * + * All ezcMvcRequest object returned by including fixture_request.php + * will be fed to the request filter; the resulting request object will be + * tested against the ezcMvcRequest object returned by including + * expected_request.php + * + * The directory name is used to identify the current fixture set in case + * of failure. + * + * @return void + */ + static public function getFooRequestFixtures() + { + $fixtures = array(); + $directoryIterator = new DirectoryIterator( $this->fixturePath ); + + foreach( $directoryIterator as $fixtureDirectory ) + { + if ( $fixtureDirectory->isDot() ) + { + continue; + } + + $testsRoot = $this->fixturePath . DIRECTORY_SEPARATOR . $fixtureDirectory; + $expectedFile = testsRoot . DIRECTORY_SEPARATOR . 'expected_request.php'; + $expectedRequest = require $expectedFile; + if ( !$expectedRequest instanceof ezcMvcRequest ) + { + throw new Exception( "$expectedFile does not return an ezcMvcRequest object" ); + } + + $fixtureFile = testsRoot . DIRECTORY_SEPARATOR . 'fixture_request.php'; + $fixtureRequest = require $fixtureFile; + if ( !$fixtureRequest instanceof ezcMvcRequest ) + { + throw new Exception( "$fixtureFile does not return an ezcMvcRequest object" ); + } + $fixtures[] = array( $expectedRequest, $fixtureRequest ); + } + + return $fixtures; + } + +New FooFilter behaviours can be tested with touching the test-case code! +Adding new set of fixtures in new sub-directories of $fixturesPath is enough. +Use case example: in 2030, a bug in the filter is discovered with a particular +request. The programmer can export the guilty request into +/tests/foofilter/bugNumber123567/fixture_request.php and write the expected +resulting request object in +/tests/foofilter/bugNumber123567/expected_request.php. Then, FooFilter can be +hacked until all tests pass, the old ones and the new one. + +You are free to modify this example and implement a file "options.php" that +would return an array of options to pass for a specific set of fixtures. + +Unlike some MVC design patterns (ie. ZF), MvcTools authors strongly discourages +coupling processes that are not specific to actions into controllers because it +will make code: + +- reusabile, +- readable, +- testable. +
-- Components mailing list Components@lists.ez.no http://lists.ez.no/mailman/listinfo/components