From 238512b89a5f25df1ff79be064f088ad5ad7c317 Mon Sep 17 00:00:00 2001 From: Matthew Weier O'Phinney Date: Fri, 31 Aug 2012 14:44:59 -0500 Subject: [PATCH] [zendframework/zf2#2284][ZF2-507] Updated README - Notice about Date header --- .coveralls.yml | 3 + .gitattributes | 6 + .gitignore | 14 + .php_cs | 43 + .travis.yml | 35 + CONTRIBUTING.md | 229 +++++ LICENSE.txt | 27 + README.md | 9 + composer.json | 65 ++ phpunit.xml.dist | 34 + phpunit.xml.travis | 34 + src/Exception/BadMethodCallException.php | 23 + src/Exception/DomainException.php | 23 + src/Exception/ExceptionInterface.php | 18 + src/Exception/InvalidArgumentException.php | 23 + src/Exception/InvalidHelperException.php | 23 + src/Exception/RuntimeException.php | 23 + src/Helper/AbstractHelper.php | 51 + src/Helper/AbstractHtmlElement.php | 128 +++ src/Helper/BasePath.php | 62 ++ src/Helper/Cycle.php | 214 +++++ src/Helper/DeclareVars.php | 83 ++ src/Helper/Doctype.php | 230 +++++ src/Helper/EscapeCss.php | 36 + src/Helper/EscapeHtml.php | 36 + src/Helper/EscapeHtmlAttr.php | 36 + src/Helper/EscapeJs.php | 36 + src/Helper/EscapeUrl.php | 36 + src/Helper/Escaper/AbstractHelper.php | 138 +++ src/Helper/Gravatar.php | 351 +++++++ src/Helper/HeadLink.php | 425 +++++++++ src/Helper/HeadMeta.php | 419 +++++++++ src/Helper/HeadScript.php | 494 ++++++++++ src/Helper/HeadStyle.php | 408 ++++++++ src/Helper/HeadTitle.php | 256 +++++ src/Helper/HelperInterface.php | 37 + src/Helper/HtmlFlash.php | 44 + src/Helper/HtmlList.php | 62 ++ src/Helper/HtmlObject.php | 70 ++ src/Helper/HtmlPage.php | 59 ++ src/Helper/HtmlQuicktime.php | 66 ++ src/Helper/InlineScript.php | 45 + src/Helper/Json.php | 59 ++ src/Helper/Layout.php | 101 ++ src/Helper/Navigation.php | 343 +++++++ src/Helper/Navigation/AbstractHelper.php | 880 ++++++++++++++++++ src/Helper/Navigation/Breadcrumbs.php | 298 ++++++ src/Helper/Navigation/HelperInterface.php | 150 +++ src/Helper/Navigation/Links.php | 773 +++++++++++++++ src/Helper/Navigation/Menu.php | 659 +++++++++++++ src/Helper/Navigation/PluginManager.php | 63 ++ src/Helper/Navigation/Sitemap.php | 461 +++++++++ src/Helper/PaginationControl.php | 136 +++ src/Helper/Partial.php | 117 +++ src/Helper/PartialLoop.php | 75 ++ src/Helper/Placeholder.php | 74 ++ src/Helper/Placeholder/Container.php | 21 + .../Container/AbstractContainer.php | 366 ++++++++ .../Container/AbstractStandalone.php | 317 +++++++ src/Helper/Placeholder/Registry.php | 178 ++++ src/Helper/RenderChildModel.php | 126 +++ src/Helper/RenderToPlaceholder.php | 37 + src/Helper/ServerUrl.php | 142 +++ src/Helper/Url.php | 114 +++ src/Helper/ViewModel.php | 96 ++ src/HelperPluginManager.php | 168 ++++ src/Model/ConsoleModel.php | 93 ++ src/Model/FeedModel.php | 94 ++ src/Model/JsonModel.php | 76 ++ src/Model/ModelInterface.php | 173 ++++ src/Model/ViewModel.php | 438 +++++++++ src/Renderer/ConsoleRenderer.php | 155 +++ src/Renderer/FeedRenderer.php | 141 +++ src/Renderer/JsonRenderer.php | 223 +++++ src/Renderer/PhpRenderer.php | 516 ++++++++++ src/Renderer/RendererInterface.php | 51 + src/Renderer/TreeRendererInterface.php | 26 + src/Resolver/AggregateResolver.php | 142 +++ src/Resolver/ResolverInterface.php | 30 + src/Resolver/TemplateMapResolver.php | 184 ++++ src/Resolver/TemplatePathStack.php | 339 +++++++ src/Strategy/FeedStrategy.php | 168 ++++ src/Strategy/JsonStrategy.php | 153 +++ src/Strategy/PhpRendererStrategy.php | 160 ++++ src/Stream.php | 172 ++++ src/Variables.php | 165 ++++ src/View.php | 261 ++++++ src/ViewEvent.php | 262 ++++++ test/Helper/AbstractTest.php | 43 + test/Helper/BasePathTest.php | 56 ++ test/Helper/CycleTest.php | 135 +++ test/Helper/DeclareVarsTest.php | 84 ++ test/Helper/DoctypeTest.php | 205 ++++ test/Helper/EscapeCssTest.php | 203 ++++ test/Helper/EscapeHtmlAttrTest.php | 203 ++++ test/Helper/EscapeHtmlTest.php | 200 ++++ test/Helper/EscapeJsTest.php | 203 ++++ test/Helper/EscapeUrlTest.php | 203 ++++ test/Helper/GravatarTest.php | 265 ++++++ test/Helper/HeadLinkTest.php | 413 ++++++++ test/Helper/HeadMetaTest.php | 491 ++++++++++ test/Helper/HeadScriptTest.php | 452 +++++++++ test/Helper/HeadStyleTest.php | 418 +++++++++ test/Helper/HeadTitleTest.php | 231 +++++ test/Helper/HtmlFlashTest.php | 58 ++ test/Helper/HtmlListTest.php | 225 +++++ test/Helper/HtmlObjectTest.php | 119 +++ test/Helper/HtmlPageTest.php | 59 ++ test/Helper/HtmlQuicktimeTest.php | 60 ++ test/Helper/InlineScriptTest.php | 78 ++ test/Helper/JsonTest.php | 63 ++ test/Helper/LayoutTest.php | 80 ++ test/Helper/Navigation/AbstractTest.php | 207 ++++ test/Helper/Navigation/BreadcrumbsTest.php | 236 +++++ test/Helper/Navigation/LinksTest.php | 729 +++++++++++++++ test/Helper/Navigation/MenuTest.php | 532 +++++++++++ test/Helper/Navigation/NavigationTest.php | 453 +++++++++ test/Helper/Navigation/SitemapTest.php | 262 ++++++ .../Navigation/_files/expected/bc/acl.html | 1 + .../_files/expected/bc/default.html | 1 + .../_files/expected/bc/linklast.html | 1 + .../_files/expected/bc/maxdepth.html | 1 + .../_files/expected/bc/partial.html | 1 + .../_files/expected/bc/separator.html | 1 + .../_files/expected/bc/translated.html | 1 + .../_files/expected/links/default.html | 7 + .../Navigation/_files/expected/menu/acl.html | 65 ++ .../expected/menu/acl_role_interface.html | 62 ++ .../_files/expected/menu/acl_string.html | 62 ++ .../_files/expected/menu/bothdepts.html | 52 ++ .../Navigation/_files/expected/menu/css.html | 11 + .../_files/expected/menu/default1.html | 81 ++ .../_files/expected/menu/default2.html | 11 + .../expected/menu/escapelabels_as_false.html | 14 + .../expected/menu/escapelabels_as_true.html | 14 + .../_files/expected/menu/indent4.html | 81 ++ .../_files/expected/menu/indent8.html | 81 ++ .../_files/expected/menu/maxdepth.html | 44 + .../_files/expected/menu/mindepth.html | 60 ++ .../expected/menu/onlyactivebranch.html | 31 + .../menu/onlyactivebranch_bothdepts.html | 21 + .../menu/onlyactivebranch_maxdepth.html | 26 + .../menu/onlyactivebranch_mindepth.html | 26 + .../menu/onlyactivebranch_noparents.html | 8 + .../expected/menu/onlyactivebranch_np_bd.html | 8 + .../menu/onlyactivebranch_np_bd2.html | 11 + .../_files/expected/menu/partial.html | 2 + .../_files/expected/menu/translated.html | 81 ++ .../_files/expected/sitemap/acl.xml | 54 ++ .../_files/expected/sitemap/acl2.xml | 39 + .../_files/expected/sitemap/default1.xml | 66 ++ .../_files/expected/sitemap/default2.xml | 14 + .../_files/expected/sitemap/depth1.xml | 18 + .../_files/expected/sitemap/depth2.xml | 51 + .../_files/expected/sitemap/depth3.xml | 45 + .../_files/expected/sitemap/invalid.xml | 19 + .../_files/expected/sitemap/nodecl.xml | 13 + .../_files/expected/sitemap/serverurl1.xml | 66 ++ .../_files/expected/sitemap/serverurl2.xml | 66 ++ .../Navigation/_files/mvc/views/bc.phtml | 4 + .../Navigation/_files/mvc/views/menu.phtml | 13 + test/Helper/Navigation/_files/navigation.xml | 212 +++++ test/Helper/PaginationControlTest.php | 182 ++++ test/Helper/PartialLoopTest.php | 457 +++++++++ test/Helper/PartialTest.php | 220 +++++ test/Helper/Placeholder/ContainerTest.php | 415 +++++++++ test/Helper/Placeholder/RegistryTest.php | 185 ++++ .../Placeholder/StandaloneContainerTest.php | 78 ++ test/Helper/PlaceholderTest.php | 91 ++ test/Helper/RenderChildModelTest.php | 133 +++ test/Helper/RenderToPlaceholderTest.php | 42 + test/Helper/ServerUrlTest.php | 186 ++++ test/Helper/TestAsset/ArrayTranslator.php | 24 + test/Helper/TestAsset/ConcreteHelper.php | 21 + test/Helper/TestAsset/Stringified.php | 19 + test/Helper/TestAsset/ToArray.php | 21 + test/Helper/UrlTest.php | 135 +++ .../views/scripts/action-bar/baz.phtml | 5 + .../views/scripts/action-foo/bar.phtml | 1 + .../views/scripts/action-foo/baz.phtml | 1 + .../views/scripts/partialActionCall.phtml | 2 + .../views/scripts/partialLoop.phtml | 2 + .../views/scripts/partialLoopCouter.phtml | 2 + .../views/scripts/partialLoopObject.phtml | 6 + .../views/scripts/partialObj.phtml | 11 + .../views/scripts/partialOne.phtml | 1 + .../views/scripts/partialThree.phtml | 2 + .../views/scripts/partialVars.phtml | 4 + .../scripts/rendertoplaceholderscript.phtml | 1 + .../_files/scripts/testPagination.phtml | 3 + test/HelperPluginManagerTest.php | 67 ++ test/Model/JsonModelTest.php | 50 + test/Model/TestAsset/Variable.php | 36 + test/Model/ViewModelTest.php | 272 ++++++ test/PhpRendererTest.php | 377 ++++++++ test/Renderer/FeedRendererTest.php | 128 +++ test/Renderer/JsonRendererTest.php | 237 +++++ test/Renderer/TestAsset/JsonModel.php | 28 + test/Resolver/AggregateResolverTest.php | 134 +++ test/Resolver/TemplateMapResolverTest.php | 203 ++++ test/Strategy/FeedStrategyTest.php | 263 ++++++ test/Strategy/JsonStrategyTest.php | 218 +++++ test/Strategy/PhpRendererStrategyTest.php | 180 ++++ test/TemplatePathStackTest.php | 256 +++++ test/TestAsset/Invokable.php | 28 + test/TestAsset/Renderer/VarExportRenderer.php | 43 + test/TestAsset/Uninvokable.php | 17 + test/TestAsset/VariableFunctor.php | 31 + test/VariablesTest.php | 128 +++ test/ViewEventTest.php | 166 ++++ test/ViewTest.php | 293 ++++++ test/_stubs/FilterDir1/Foo.php | 24 + test/_stubs/HelperDir1/Stub1.php | 24 + test/_stubs/HelperDir1/StubEmpty.php | 13 + test/_stubs/HelperDir2/Datetime.php | 24 + test/_stubs/HelperDir2/Stub1.php | 24 + test/_stubs/HelperDir2/Stub2.php | 32 + test/_stubs/scripts/LfiProtectionCheck.phtml | 3 + test/_templates/block.phtml | 6 + test/_templates/empty.phtml | 1 + test/_templates/layout.phtml | 5 + .../_templates/nested-view-model-child2.phtml | 2 + .../nested-view-model-complexlayout.phtml | 5 + .../nested-view-model-content.phtml | 1 + .../_templates/nested-view-model-layout.phtml | 6 + test/_templates/test-with-helpers.phtml | 5 + test/_templates/test.phtml | 3 + test/_templates/testLocalVars.phtml | 1 + test/_templates/testNestedInner.phtml | 1 + test/_templates/testNestedOuter.phtml | 4 + test/_templates/testParent.phtml | 7 + test/_templates/testStrictVars.phtml | 5 + test/_templates/testSubTemplate.phtml | 3 + test/_templates/testZf995.phtml | 3 + test/_templates/view-model-variables.phtml | 1 + test/_templates/view.phar | Bin 0 -> 7031 bytes test/bootstrap.php | 34 + 237 files changed, 28384 insertions(+) create mode 100644 .coveralls.yml create mode 100644 .gitattributes create mode 100644 .gitignore create mode 100644 .php_cs create mode 100644 .travis.yml create mode 100644 CONTRIBUTING.md create mode 100644 LICENSE.txt create mode 100644 README.md create mode 100644 composer.json create mode 100644 phpunit.xml.dist create mode 100644 phpunit.xml.travis create mode 100644 src/Exception/BadMethodCallException.php create mode 100644 src/Exception/DomainException.php create mode 100644 src/Exception/ExceptionInterface.php create mode 100644 src/Exception/InvalidArgumentException.php create mode 100644 src/Exception/InvalidHelperException.php create mode 100644 src/Exception/RuntimeException.php create mode 100644 src/Helper/AbstractHelper.php create mode 100644 src/Helper/AbstractHtmlElement.php create mode 100644 src/Helper/BasePath.php create mode 100644 src/Helper/Cycle.php create mode 100644 src/Helper/DeclareVars.php create mode 100644 src/Helper/Doctype.php create mode 100644 src/Helper/EscapeCss.php create mode 100644 src/Helper/EscapeHtml.php create mode 100644 src/Helper/EscapeHtmlAttr.php create mode 100644 src/Helper/EscapeJs.php create mode 100644 src/Helper/EscapeUrl.php create mode 100644 src/Helper/Escaper/AbstractHelper.php create mode 100644 src/Helper/Gravatar.php create mode 100644 src/Helper/HeadLink.php create mode 100644 src/Helper/HeadMeta.php create mode 100644 src/Helper/HeadScript.php create mode 100644 src/Helper/HeadStyle.php create mode 100644 src/Helper/HeadTitle.php create mode 100644 src/Helper/HelperInterface.php create mode 100644 src/Helper/HtmlFlash.php create mode 100644 src/Helper/HtmlList.php create mode 100644 src/Helper/HtmlObject.php create mode 100644 src/Helper/HtmlPage.php create mode 100644 src/Helper/HtmlQuicktime.php create mode 100644 src/Helper/InlineScript.php create mode 100644 src/Helper/Json.php create mode 100644 src/Helper/Layout.php create mode 100644 src/Helper/Navigation.php create mode 100644 src/Helper/Navigation/AbstractHelper.php create mode 100644 src/Helper/Navigation/Breadcrumbs.php create mode 100644 src/Helper/Navigation/HelperInterface.php create mode 100644 src/Helper/Navigation/Links.php create mode 100644 src/Helper/Navigation/Menu.php create mode 100644 src/Helper/Navigation/PluginManager.php create mode 100644 src/Helper/Navigation/Sitemap.php create mode 100644 src/Helper/PaginationControl.php create mode 100644 src/Helper/Partial.php create mode 100644 src/Helper/PartialLoop.php create mode 100644 src/Helper/Placeholder.php create mode 100644 src/Helper/Placeholder/Container.php create mode 100644 src/Helper/Placeholder/Container/AbstractContainer.php create mode 100644 src/Helper/Placeholder/Container/AbstractStandalone.php create mode 100644 src/Helper/Placeholder/Registry.php create mode 100644 src/Helper/RenderChildModel.php create mode 100644 src/Helper/RenderToPlaceholder.php create mode 100644 src/Helper/ServerUrl.php create mode 100644 src/Helper/Url.php create mode 100644 src/Helper/ViewModel.php create mode 100644 src/HelperPluginManager.php create mode 100644 src/Model/ConsoleModel.php create mode 100644 src/Model/FeedModel.php create mode 100644 src/Model/JsonModel.php create mode 100644 src/Model/ModelInterface.php create mode 100644 src/Model/ViewModel.php create mode 100644 src/Renderer/ConsoleRenderer.php create mode 100644 src/Renderer/FeedRenderer.php create mode 100644 src/Renderer/JsonRenderer.php create mode 100644 src/Renderer/PhpRenderer.php create mode 100644 src/Renderer/RendererInterface.php create mode 100644 src/Renderer/TreeRendererInterface.php create mode 100644 src/Resolver/AggregateResolver.php create mode 100644 src/Resolver/ResolverInterface.php create mode 100644 src/Resolver/TemplateMapResolver.php create mode 100644 src/Resolver/TemplatePathStack.php create mode 100644 src/Strategy/FeedStrategy.php create mode 100644 src/Strategy/JsonStrategy.php create mode 100644 src/Strategy/PhpRendererStrategy.php create mode 100644 src/Stream.php create mode 100644 src/Variables.php create mode 100644 src/View.php create mode 100644 src/ViewEvent.php create mode 100644 test/Helper/AbstractTest.php create mode 100644 test/Helper/BasePathTest.php create mode 100644 test/Helper/CycleTest.php create mode 100644 test/Helper/DeclareVarsTest.php create mode 100644 test/Helper/DoctypeTest.php create mode 100644 test/Helper/EscapeCssTest.php create mode 100644 test/Helper/EscapeHtmlAttrTest.php create mode 100644 test/Helper/EscapeHtmlTest.php create mode 100644 test/Helper/EscapeJsTest.php create mode 100644 test/Helper/EscapeUrlTest.php create mode 100644 test/Helper/GravatarTest.php create mode 100644 test/Helper/HeadLinkTest.php create mode 100644 test/Helper/HeadMetaTest.php create mode 100644 test/Helper/HeadScriptTest.php create mode 100644 test/Helper/HeadStyleTest.php create mode 100644 test/Helper/HeadTitleTest.php create mode 100644 test/Helper/HtmlFlashTest.php create mode 100644 test/Helper/HtmlListTest.php create mode 100644 test/Helper/HtmlObjectTest.php create mode 100644 test/Helper/HtmlPageTest.php create mode 100644 test/Helper/HtmlQuicktimeTest.php create mode 100644 test/Helper/InlineScriptTest.php create mode 100644 test/Helper/JsonTest.php create mode 100644 test/Helper/LayoutTest.php create mode 100644 test/Helper/Navigation/AbstractTest.php create mode 100644 test/Helper/Navigation/BreadcrumbsTest.php create mode 100644 test/Helper/Navigation/LinksTest.php create mode 100644 test/Helper/Navigation/MenuTest.php create mode 100644 test/Helper/Navigation/NavigationTest.php create mode 100644 test/Helper/Navigation/SitemapTest.php create mode 100644 test/Helper/Navigation/_files/expected/bc/acl.html create mode 100644 test/Helper/Navigation/_files/expected/bc/default.html create mode 100644 test/Helper/Navigation/_files/expected/bc/linklast.html create mode 100644 test/Helper/Navigation/_files/expected/bc/maxdepth.html create mode 100644 test/Helper/Navigation/_files/expected/bc/partial.html create mode 100644 test/Helper/Navigation/_files/expected/bc/separator.html create mode 100644 test/Helper/Navigation/_files/expected/bc/translated.html create mode 100644 test/Helper/Navigation/_files/expected/links/default.html create mode 100644 test/Helper/Navigation/_files/expected/menu/acl.html create mode 100644 test/Helper/Navigation/_files/expected/menu/acl_role_interface.html create mode 100644 test/Helper/Navigation/_files/expected/menu/acl_string.html create mode 100644 test/Helper/Navigation/_files/expected/menu/bothdepts.html create mode 100644 test/Helper/Navigation/_files/expected/menu/css.html create mode 100644 test/Helper/Navigation/_files/expected/menu/default1.html create mode 100644 test/Helper/Navigation/_files/expected/menu/default2.html create mode 100644 test/Helper/Navigation/_files/expected/menu/escapelabels_as_false.html create mode 100644 test/Helper/Navigation/_files/expected/menu/escapelabels_as_true.html create mode 100644 test/Helper/Navigation/_files/expected/menu/indent4.html create mode 100644 test/Helper/Navigation/_files/expected/menu/indent8.html create mode 100644 test/Helper/Navigation/_files/expected/menu/maxdepth.html create mode 100644 test/Helper/Navigation/_files/expected/menu/mindepth.html create mode 100644 test/Helper/Navigation/_files/expected/menu/onlyactivebranch.html create mode 100644 test/Helper/Navigation/_files/expected/menu/onlyactivebranch_bothdepts.html create mode 100644 test/Helper/Navigation/_files/expected/menu/onlyactivebranch_maxdepth.html create mode 100644 test/Helper/Navigation/_files/expected/menu/onlyactivebranch_mindepth.html create mode 100644 test/Helper/Navigation/_files/expected/menu/onlyactivebranch_noparents.html create mode 100644 test/Helper/Navigation/_files/expected/menu/onlyactivebranch_np_bd.html create mode 100644 test/Helper/Navigation/_files/expected/menu/onlyactivebranch_np_bd2.html create mode 100644 test/Helper/Navigation/_files/expected/menu/partial.html create mode 100644 test/Helper/Navigation/_files/expected/menu/translated.html create mode 100644 test/Helper/Navigation/_files/expected/sitemap/acl.xml create mode 100644 test/Helper/Navigation/_files/expected/sitemap/acl2.xml create mode 100644 test/Helper/Navigation/_files/expected/sitemap/default1.xml create mode 100644 test/Helper/Navigation/_files/expected/sitemap/default2.xml create mode 100644 test/Helper/Navigation/_files/expected/sitemap/depth1.xml create mode 100644 test/Helper/Navigation/_files/expected/sitemap/depth2.xml create mode 100644 test/Helper/Navigation/_files/expected/sitemap/depth3.xml create mode 100644 test/Helper/Navigation/_files/expected/sitemap/invalid.xml create mode 100644 test/Helper/Navigation/_files/expected/sitemap/nodecl.xml create mode 100644 test/Helper/Navigation/_files/expected/sitemap/serverurl1.xml create mode 100644 test/Helper/Navigation/_files/expected/sitemap/serverurl2.xml create mode 100644 test/Helper/Navigation/_files/mvc/views/bc.phtml create mode 100644 test/Helper/Navigation/_files/mvc/views/menu.phtml create mode 100644 test/Helper/Navigation/_files/navigation.xml create mode 100644 test/Helper/PaginationControlTest.php create mode 100644 test/Helper/PartialLoopTest.php create mode 100644 test/Helper/PartialTest.php create mode 100644 test/Helper/Placeholder/ContainerTest.php create mode 100644 test/Helper/Placeholder/RegistryTest.php create mode 100644 test/Helper/Placeholder/StandaloneContainerTest.php create mode 100644 test/Helper/PlaceholderTest.php create mode 100644 test/Helper/RenderChildModelTest.php create mode 100644 test/Helper/RenderToPlaceholderTest.php create mode 100644 test/Helper/ServerUrlTest.php create mode 100644 test/Helper/TestAsset/ArrayTranslator.php create mode 100644 test/Helper/TestAsset/ConcreteHelper.php create mode 100644 test/Helper/TestAsset/Stringified.php create mode 100644 test/Helper/TestAsset/ToArray.php create mode 100644 test/Helper/UrlTest.php create mode 100644 test/Helper/_files/modules/application/views/scripts/action-bar/baz.phtml create mode 100644 test/Helper/_files/modules/application/views/scripts/action-foo/bar.phtml create mode 100644 test/Helper/_files/modules/application/views/scripts/action-foo/baz.phtml create mode 100644 test/Helper/_files/modules/application/views/scripts/partialActionCall.phtml create mode 100644 test/Helper/_files/modules/application/views/scripts/partialLoop.phtml create mode 100644 test/Helper/_files/modules/application/views/scripts/partialLoopCouter.phtml create mode 100644 test/Helper/_files/modules/application/views/scripts/partialLoopObject.phtml create mode 100644 test/Helper/_files/modules/application/views/scripts/partialObj.phtml create mode 100644 test/Helper/_files/modules/application/views/scripts/partialOne.phtml create mode 100644 test/Helper/_files/modules/application/views/scripts/partialThree.phtml create mode 100644 test/Helper/_files/modules/application/views/scripts/partialVars.phtml create mode 100644 test/Helper/_files/scripts/rendertoplaceholderscript.phtml create mode 100644 test/Helper/_files/scripts/testPagination.phtml create mode 100644 test/HelperPluginManagerTest.php create mode 100644 test/Model/JsonModelTest.php create mode 100644 test/Model/TestAsset/Variable.php create mode 100644 test/Model/ViewModelTest.php create mode 100644 test/PhpRendererTest.php create mode 100644 test/Renderer/FeedRendererTest.php create mode 100644 test/Renderer/JsonRendererTest.php create mode 100644 test/Renderer/TestAsset/JsonModel.php create mode 100644 test/Resolver/AggregateResolverTest.php create mode 100644 test/Resolver/TemplateMapResolverTest.php create mode 100644 test/Strategy/FeedStrategyTest.php create mode 100644 test/Strategy/JsonStrategyTest.php create mode 100644 test/Strategy/PhpRendererStrategyTest.php create mode 100644 test/TemplatePathStackTest.php create mode 100644 test/TestAsset/Invokable.php create mode 100644 test/TestAsset/Renderer/VarExportRenderer.php create mode 100644 test/TestAsset/Uninvokable.php create mode 100644 test/TestAsset/VariableFunctor.php create mode 100644 test/VariablesTest.php create mode 100644 test/ViewEventTest.php create mode 100644 test/ViewTest.php create mode 100644 test/_stubs/FilterDir1/Foo.php create mode 100644 test/_stubs/HelperDir1/Stub1.php create mode 100644 test/_stubs/HelperDir1/StubEmpty.php create mode 100644 test/_stubs/HelperDir2/Datetime.php create mode 100644 test/_stubs/HelperDir2/Stub1.php create mode 100644 test/_stubs/HelperDir2/Stub2.php create mode 100644 test/_stubs/scripts/LfiProtectionCheck.phtml create mode 100644 test/_templates/block.phtml create mode 100644 test/_templates/empty.phtml create mode 100644 test/_templates/layout.phtml create mode 100644 test/_templates/nested-view-model-child2.phtml create mode 100644 test/_templates/nested-view-model-complexlayout.phtml create mode 100644 test/_templates/nested-view-model-content.phtml create mode 100644 test/_templates/nested-view-model-layout.phtml create mode 100644 test/_templates/test-with-helpers.phtml create mode 100644 test/_templates/test.phtml create mode 100644 test/_templates/testLocalVars.phtml create mode 100644 test/_templates/testNestedInner.phtml create mode 100644 test/_templates/testNestedOuter.phtml create mode 100644 test/_templates/testParent.phtml create mode 100644 test/_templates/testStrictVars.phtml create mode 100644 test/_templates/testSubTemplate.phtml create mode 100644 test/_templates/testZf995.phtml create mode 100644 test/_templates/view-model-variables.phtml create mode 100644 test/_templates/view.phar create mode 100644 test/bootstrap.php diff --git a/.coveralls.yml b/.coveralls.yml new file mode 100644 index 00000000..53bda829 --- /dev/null +++ b/.coveralls.yml @@ -0,0 +1,3 @@ +coverage_clover: clover.xml +json_path: coveralls-upload.json +src_dir: src diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 00000000..85dc9a8c --- /dev/null +++ b/.gitattributes @@ -0,0 +1,6 @@ +/test export-ignore +/vendor export-ignore +.gitattributes export-ignore +.gitignore export-ignore +.travis.yml export-ignore +.php_cs export-ignore diff --git a/.gitignore b/.gitignore new file mode 100644 index 00000000..4cac0a21 --- /dev/null +++ b/.gitignore @@ -0,0 +1,14 @@ +.buildpath +.DS_Store +.idea +.project +.settings/ +.*.sw* +.*.un~ +nbproject +tmp/ + +clover.xml +coveralls-upload.json +phpunit.xml +vendor diff --git a/.php_cs b/.php_cs new file mode 100644 index 00000000..bf4b799f --- /dev/null +++ b/.php_cs @@ -0,0 +1,43 @@ +notPath('TestAsset') + ->notPath('_files') + ->filter(function (SplFileInfo $file) { + if (strstr($file->getPath(), 'compatibility')) { + return false; + } + }); +$config = Symfony\CS\Config\Config::create(); +$config->level(null); +$config->fixers( + array( + 'braces', + 'duplicate_semicolon', + 'elseif', + 'empty_return', + 'encoding', + 'eof_ending', + 'function_call_space', + 'function_declaration', + 'indentation', + 'join_function', + 'line_after_namespace', + 'linefeed', + 'lowercase_keywords', + 'parenthesis', + 'multiple_use', + 'method_argument_space', + 'object_operator', + 'php_closing_tag', + 'psr0', + 'remove_lines_between_uses', + 'short_tag', + 'standardize_not_equal', + 'trailing_spaces', + 'unused_use', + 'visibility', + 'whitespacy_lines', + ) +); +$config->finder($finder); +return $config; diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 00000000..fe909ecb --- /dev/null +++ b/.travis.yml @@ -0,0 +1,35 @@ +sudo: false + +language: php + +matrix: + fast_finish: true + include: + - php: 5.5 + - php: 5.6 + env: + - EXECUTE_TEST_COVERALLS=true + - EXECUTE_CS_CHECK=true + - php: 7 + - php: hhvm + allow_failures: + - php: 7 + - php: hhvm + +notifications: + irc: "irc.freenode.org#zftalk.dev" + email: false + +before_install: + - if [[ $EXECUTE_TEST_COVERALLS != 'true' ]]; then phpenv config-rm xdebug.ini || return 0 ; fi + +install: + - composer install --no-interaction --prefer-source + +script: + - if [[ $EXECUTE_TEST_COVERALLS == 'true' ]]; then ./vendor/bin/phpunit -c phpunit.xml.travis --coverage-clover clover.xml ; fi + - if [[ $EXECUTE_TEST_COVERALLS != 'true' ]]; then ./vendor/bin/phpunit -c phpunit.xml.travis ; fi + - if [[ $EXECUTE_CS_CHECK == 'true' ]]; then ./vendor/bin/php-cs-fixer fix -v --diff --dry-run --config-file=.php_cs ; fi + +after_script: + - if [[ $EXECUTE_TEST_COVERALLS == 'true' ]]; then ./vendor/bin/coveralls ; fi diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 00000000..e120cdbd --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,229 @@ +# CONTRIBUTING + +## RESOURCES + +If you wish to contribute to Zend Framework, please be sure to +read/subscribe to the following resources: + + - [Coding Standards](https://github.com/zendframework/zf2/wiki/Coding-Standards) + - [Contributor's Guide](http://framework.zend.com/participate/contributor-guide) + - ZF Contributor's mailing list: + Archives: http://zend-framework-community.634137.n4.nabble.com/ZF-Contributor-f680267.html + Subscribe: zf-contributors-subscribe@lists.zend.com + - ZF Contributor's IRC channel: + #zftalk.dev on Freenode.net + +If you are working on new features or refactoring [create a proposal](https://github.com/zendframework/zend-view/issues/new). + +## Reporting Potential Security Issues + +If you have encountered a potential security vulnerability, please **DO NOT** report it on the public +issue tracker: send it to us at [zf-security@zend.com](mailto:zf-security@zend.com) instead. +We will work with you to verify the vulnerability and patch it as soon as possible. + +When reporting issues, please provide the following information: + +- Component(s) affected +- A description indicating how to reproduce the issue +- A summary of the security vulnerability and impact + +We request that you contact us via the email address above and give the project +contributors a chance to resolve the vulnerability and issue a new release prior +to any public exposure; this helps protect users and provides them with a chance +to upgrade and/or update in order to protect their applications. + +For sensitive email communications, please use [our PGP key](http://framework.zend.com/zf-security-pgp-key.asc). + +## RUNNING TESTS + +> ### Note: testing versions prior to 2.4 +> +> This component originates with Zend Framework 2. During the lifetime of ZF2, +> testing infrastructure migrated from PHPUnit 3 to PHPUnit 4. In most cases, no +> changes were necessary. However, due to the migration, tests may not run on +> versions < 2.4. As such, you may need to change the PHPUnit dependency if +> attempting a fix on such a version. + +To run tests: + +- Clone the repository: + + ```console + $ git clone git@github.com:zendframework/zend-view.git + $ cd + ``` + +- Install dependencies via composer: + + ```console + $ curl -sS https://getcomposer.org/installer | php -- + $ ./composer.phar install + ``` + + If you don't have `curl` installed, you can also download `composer.phar` from https://getcomposer.org/ + +- Run the tests via `phpunit` and the provided PHPUnit config, like in this example: + + ```console + $ ./vendor/bin/phpunit + ``` + +You can turn on conditional tests with the phpunit.xml file. +To do so: + + - Copy `phpunit.xml.dist` file to `phpunit.xml` + - Edit `phpunit.xml` to enable any specific functionality you + want to test, as well as to provide test values to utilize. + +## Running Coding Standards Checks + +This component uses [php-cs-fixer](http://cs.sensiolabs.org/) for coding +standards checks, and provides configuration for our selected checks. +`php-cs-fixer` is installed by default via Composer. + +To run checks only: + +```console +$ ./vendor/bin/php-cs-fixer fix . -v --diff --dry-run --config-file=.php_cs +``` + +To have `php-cs-fixer` attempt to fix problems for you, omit the `--dry-run` +flag: + +```console +$ ./vendor/bin/php-cs-fixer fix . -v --diff --config-file=.php_cs +``` + +If you allow php-cs-fixer to fix CS issues, please re-run the tests to ensure +they pass, and make sure you add and commit the changes after verification. + +## Recommended Workflow for Contributions + +Your first step is to establish a public repository from which we can +pull your work into the master repository. We recommend using +[GitHub](https://github.com), as that is where the component is already hosted. + +1. Setup a [GitHub account](http://github.com/), if you haven't yet +2. Fork the repository (http://github.com/zendframework/zend-view) +3. Clone the canonical repository locally and enter it. + + ```console + $ git clone git://github.com:zendframework/zend-view.git + $ cd zend-view + ``` + +4. Add a remote to your fork; substitute your GitHub username in the command + below. + + ```console + $ git remote add {username} git@github.com:{username}/zend-view.git + $ git fetch {username} + ``` + +### Keeping Up-to-Date + +Periodically, you should update your fork or personal repository to +match the canonical ZF repository. Assuming you have setup your local repository +per the instructions above, you can do the following: + + +```console +$ git checkout master +$ git fetch origin +$ git rebase origin/master +# OPTIONALLY, to keep your remote up-to-date - +$ git push {username} master:master +``` + +If you're tracking other branches -- for example, the "develop" branch, where +new feature development occurs -- you'll want to do the same operations for that +branch; simply substitute "develop" for "master". + +### Working on a patch + +We recommend you do each new feature or bugfix in a new branch. This simplifies +the task of code review as well as the task of merging your changes into the +canonical repository. + +A typical workflow will then consist of the following: + +1. Create a new local branch based off either your master or develop branch. +2. Switch to your new local branch. (This step can be combined with the + previous step with the use of `git checkout -b`.) +3. Do some work, commit, repeat as necessary. +4. Push the local branch to your remote repository. +5. Send a pull request. + +The mechanics of this process are actually quite trivial. Below, we will +create a branch for fixing an issue in the tracker. + +```console +$ git checkout -b hotfix/9295 +Switched to a new branch 'hotfix/9295' +``` + +... do some work ... + + +```console +$ git commit +``` + +... write your log message ... + + +```console +$ git push {username} hotfix/9295:hotfix/9295 +Counting objects: 38, done. +Delta compression using up to 2 threads. +Compression objects: 100% (18/18), done. +Writing objects: 100% (20/20), 8.19KiB, done. +Total 20 (delta 12), reused 0 (delta 0) +To ssh://git@github.com/{username}/zend-view.git + b5583aa..4f51698 HEAD -> master +``` + +To send a pull request, you have two options. + +If using GitHub, you can do the pull request from there. Navigate to +your repository, select the branch you just created, and then select the +"Pull Request" button in the upper right. Select the user/organization +"zendframework" as the recipient. + +If using your own repository - or even if using GitHub - you can use `git +format-patch` to create a patchset for us to apply; in fact, this is +**recommended** for security-related patches. If you use `format-patch`, please +send the patches as attachments to: + +- zf-devteam@zend.com for patches without security implications +- zf-security@zend.com for security patches + +#### What branch to issue the pull request against? + +Which branch should you issue a pull request against? + +- For fixes against the stable release, issue the pull request against the + "master" branch. +- For new features, or fixes that introduce new elements to the public API (such + as new public methods or properties), issue the pull request against the + "develop" branch. + +### Branch Cleanup + +As you might imagine, if you are a frequent contributor, you'll start to +get a ton of branches both locally and on your remote. + +Once you know that your changes have been accepted to the master +repository, we suggest doing some cleanup of these branches. + +- Local branch cleanup + + ```console + $ git branch -d + ``` + +- Remote branch removal + + ```console + $ git push {username} : + ``` diff --git a/LICENSE.txt b/LICENSE.txt new file mode 100644 index 00000000..6eab5aa1 --- /dev/null +++ b/LICENSE.txt @@ -0,0 +1,27 @@ +Copyright (c) 2005-2015, Zend Technologies USA, Inc. +All rights reserved. + +Redistribution and use in source and binary forms, with or without modification, +are permitted provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright notice, + this list of conditions and the following disclaimer. + + * Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + + * Neither the name of Zend Technologies USA, Inc. nor the names of its + contributors may be used to endorse or promote products derived from this + software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR +ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON +ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/README.md b/README.md new file mode 100644 index 00000000..0aee9d06 --- /dev/null +++ b/README.md @@ -0,0 +1,9 @@ +# zend-view + +`Zend\View` provides the “View” layer of Zend Framework 2’s MVC system. It is a +multi-tiered system allowing a variety of mechanisms for extension, +substitution, and more. + + +- File issues at https://github.com/zendframework/zend-view/issues +- Documentation is at http://framework.zend.com/manual/current/en/index.html#zend-view diff --git a/composer.json b/composer.json new file mode 100644 index 00000000..032477d2 --- /dev/null +++ b/composer.json @@ -0,0 +1,65 @@ +{ + "name": "zendframework/zend-view", + "description": "provides a system of helpers, output filters, and variable escaping", + "license": "BSD-3-Clause", + "keywords": [ + "zf2", + "view" + ], + "homepage": "https://github.com/zendframework/zend-view", + "autoload": { + "psr-4": { + "Zend\\View": "src/" + } + }, + "require": { + "php": ">=5.3.3", + "zendframework/zend-eventmanager": "self.version", + "zendframework/zend-loader": "self.version", + "zendframework/zend-stdlib": "self.version" + }, + "require-dev": { + "zendframework/zend-authentication": "self.version", + "zendframework/zend-escaper": "self.version", + "zendframework/zend-feed": "self.version", + "zendframework/zend-filter": "self.version", + "zendframework/zend-http": "self.version", + "zendframework/zend-i18n": "self.version", + "zendframework/zend-json": "self.version", + "zendframework/zend-mvc": "self.version", + "zendframework/zend-navigation": "self.version", + "zendframework/zend-paginator": "self.version", + "zendframework/zend-permissions-acl": "self.version", + "zendframework/zend-servicemanager": "self.version", + "zendframework/zend-uri": "self.version", + "fabpot/php-cs-fixer": "1.7.*", + "satooshi/php-coveralls": "dev-master", + "phpunit/PHPUnit": "~4.0" + }, + "suggest": { + "zendframework/zend-authentication": "Zend\\Authentication component", + "zendframework/zend-escaper": "Zend\\Escaper component", + "zendframework/zend-feed": "Zend\\Feed component", + "zendframework/zend-filter": "Zend\\Filter component", + "zendframework/zend-http": "Zend\\Http component", + "zendframework/zend-i18n": "Zend\\I18n component", + "zendframework/zend-json": "Zend\\Json component", + "zendframework/zend-mvc": "Zend\\Mvc component", + "zendframework/zend-navigation": "Zend\\Navigation component", + "zendframework/zend-paginator": "Zend\\Paginator component", + "zendframework/zend-permissions-acl": "Zend\\Permissions\\Acl component", + "zendframework/zend-servicemanager": "Zend\\ServiceManager component", + "zendframework/zend-uri": "Zend\\Uri component" + }, + "extra": { + "branch-alias": { + "dev-master": "2.4-dev", + "dev-develop": "2.5-dev" + } + }, + "autoload-dev": { + "psr-4": { + "ZendTest\\View\\": "test/" + } + } +} \ No newline at end of file diff --git a/phpunit.xml.dist b/phpunit.xml.dist new file mode 100644 index 00000000..2188aa87 --- /dev/null +++ b/phpunit.xml.dist @@ -0,0 +1,34 @@ + + + + + ./test/ + + + + + + disable + + + + + + ./src + + + + + + + + + + + diff --git a/phpunit.xml.travis b/phpunit.xml.travis new file mode 100644 index 00000000..2188aa87 --- /dev/null +++ b/phpunit.xml.travis @@ -0,0 +1,34 @@ + + + + + ./test/ + + + + + + disable + + + + + + ./src + + + + + + + + + + + diff --git a/src/Exception/BadMethodCallException.php b/src/Exception/BadMethodCallException.php new file mode 100644 index 00000000..3b863e1f --- /dev/null +++ b/src/Exception/BadMethodCallException.php @@ -0,0 +1,23 @@ +view = $view; + return $this; + } + + /** + * Get the view object + * + * @return null|Renderer + */ + public function getView() + { + return $this->view; + } +} diff --git a/src/Helper/AbstractHtmlElement.php b/src/Helper/AbstractHtmlElement.php new file mode 100644 index 00000000..8df277e5 --- /dev/null +++ b/src/Helper/AbstractHtmlElement.php @@ -0,0 +1,128 @@ +closingBracket) { + if ($this->isXhtml()) { + $this->closingBracket = ' />'; + } else { + $this->closingBracket = '>'; + } + } + + return $this->closingBracket; + } + + /** + * Is doctype XHTML? + * + * @return boolean + */ + protected function isXhtml() + { + $doctype = $this->view->plugin('doctype'); + return $doctype->isXhtml(); + } + + /** + * Converts an associative array to a string of tag attributes. + * + * @access public + * + * @param array $attribs From this array, each key-value pair is + * converted to an attribute name and value. + * + * @return string The XHTML for the attributes. + */ + protected function htmlAttribs($attribs) + { + $xhtml = ''; + $escaper = $this->view->plugin('escapehtml'); + foreach ((array) $attribs as $key => $val) { + $key = $escaper($key); + + if (('on' == substr($key, 0, 2)) || ('constraints' == $key)) { + // Don't escape event attributes; _do_ substitute double quotes with singles + if (!is_scalar($val)) { + // non-scalar data should be cast to JSON first + $val = \Zend\Json\Json::encode($val); + } + // Escape single quotes inside event attribute values. + // This will create html, where the attribute value has + // single quotes around it, and escaped single quotes or + // non-escaped double quotes inside of it + $val = str_replace('\'', ''', $val); + } else { + if (is_array($val)) { + $val = implode(' ', $val); + } + $val = $escaper($val); + } + + if ('id' == $key) { + $val = $this->normalizeId($val); + } + + if (strpos($val, '"') !== false) { + $xhtml .= " $key='$val'"; + } else { + $xhtml .= " $key=\"$val\""; + } + + } + return $xhtml; + } + + /** + * Normalize an ID + * + * @param string $value + * @return string + */ + protected function normalizeId($value) + { + if (strstr($value, '[')) { + if ('[]' == substr($value, -2)) { + $value = substr($value, 0, strlen($value) - 2); + } + $value = trim($value, ']'); + $value = str_replace('][', '-', $value); + $value = str_replace('[', '-', $value); + } + return $value; + } +} diff --git a/src/Helper/BasePath.php b/src/Helper/BasePath.php new file mode 100644 index 00000000..87400447 --- /dev/null +++ b/src/Helper/BasePath.php @@ -0,0 +1,62 @@ +basePath) { + throw new Exception\RuntimeException('No base path provided'); + } + + if (null !== $file) { + $file = '/' . ltrim($file, '/'); + } + + return $this->basePath . $file; + } + + /** + * Set the base path. + * + * @param string $basePath + * @return self + */ + public function setBasePath($basePath) + { + $this->basePath = rtrim($basePath, '/'); + return $this; + } +} diff --git a/src/Helper/Cycle.php b/src/Helper/Cycle.php new file mode 100644 index 00000000..1df8efcb --- /dev/null +++ b/src/Helper/Cycle.php @@ -0,0 +1,214 @@ +-1) ; + + /** + * Array of values + * + * @var array + */ + protected $data = array(self::DEFAULT_NAME=>array()); + + /** + * Actual name of cycle + * + * @var string + */ + protected $name = self::DEFAULT_NAME; + + /** + * Add elements to alternate + * + * @param array $data + * @param string $name + * @return \Zend\View\Helper\Cycle + */ + public function __invoke(array $data = array(), $name = self::DEFAULT_NAME) + { + if (!empty($data)) + $this->data[$name] = $data; + + $this->setName($name); + return $this; + } + + /** + * Add elements to alternate + * + * @param array $data + * @param string $name + * @return \Zend\View\Helper\Cycle + */ + public function assign(Array $data , $name = self::DEFAULT_NAME) + { + $this->setName($name); + $this->data[$name] = $data; + $this->rewind(); + return $this; + } + + /** + * Sets actual name of cycle + * + * @param $name + * @return \Zend\View\Helper\Cycle + */ + public function setName($name = self::DEFAULT_NAME) + { + $this->name = $name; + + if (!isset($this->data[$this->name])) + $this->data[$this->name] = array(); + + if (!isset($this->pointers[$this->name])) + $this->rewind(); + + return $this; + } + + /** + * Gets actual name of cycle + * + * @param $name + * @return string + */ + public function getName() + { + return $this->name; + } + + + /** + * Return all elements + * + * @return array + */ + public function getAll() + { + return $this->data[$this->name]; + } + + /** + * Turn helper into string + * + * @return string + */ + public function toString() + { + return (string) $this->data[$this->name][$this->key()]; + } + + /** + * Cast to string + * + * @return string + */ + public function __toString() + { + return $this->toString(); + } + + /** + * Move to next value + * + * @return \Zend\View\Helper\Cycle + */ + public function next() + { + $count = count($this->data[$this->name]); + if ($this->pointers[$this->name] == ($count - 1)) + $this->pointers[$this->name] = 0; + else + $this->pointers[$this->name] = ++$this->pointers[$this->name]; + return $this; + } + + /** + * Move to previous value + * + * @return \Zend\View\Helper\Cycle + */ + public function prev() + { + $count = count($this->data[$this->name]); + if ($this->pointers[$this->name] <= 0) + $this->pointers[$this->name] = $count - 1; + else + $this->pointers[$this->name] = --$this->pointers[$this->name]; + return $this; + } + + /** + * Return iteration number + * + * @return int + */ + public function key() + { + if ($this->pointers[$this->name] < 0) + return 0; + else + return $this->pointers[$this->name]; + } + + /** + * Rewind pointer + * + * @return \Zend\View\Helper\Cycle + */ + public function rewind() + { + $this->pointers[$this->name] = -1; + return $this; + } + + /** + * Check if element is valid + * + * @return bool + */ + public function valid() + { + return isset($this->data[$this->name][$this->key()]); + } + + /** + * Return current element + * + * @return mixed + */ + public function current() + { + return $this->data[$this->name][$this->key()]; + } +} diff --git a/src/Helper/DeclareVars.php b/src/Helper/DeclareVars.php new file mode 100644 index 00000000..84060d0a --- /dev/null +++ b/src/Helper/DeclareVars.php @@ -0,0 +1,83 @@ + + * $this->declareVars( + * 'varName1', + * 'varName2', + * array('varName3' => 'defaultValue', + * 'varName4' => array() + * ) + * ); + * + * + * @param string|array variable number of arguments, all string names of variables to test + * @return void + */ + public function __invoke() + { + $view = $this->getView(); + $args = func_get_args(); + foreach ($args as $key) { + if (is_array($key)) { + foreach ($key as $name => $value) { + $this->declareVar($name, $value); + } + } elseif (!isset($view->vars()->$key)) { + $this->declareVar($key); + } + } + } + + /** + * Set a view variable + * + * Checks to see if a $key is set in the view object; if not, sets it to $value. + * + * @param string $key + * @param string $value Defaults to an empty string + * @return void + */ + protected function declareVar($key, $value = '') + { + $view = $this->getView(); + $vars = $view->vars(); + if (!isset($vars->$key)) { + $vars->$key = $value; + } + } +} diff --git a/src/Helper/Doctype.php b/src/Helper/Doctype.php new file mode 100644 index 00000000..7322e843 --- /dev/null +++ b/src/Helper/Doctype.php @@ -0,0 +1,230 @@ + array( + self::XHTML11 => '', + self::XHTML1_STRICT => '', + self::XHTML1_TRANSITIONAL => '', + self::XHTML1_FRAMESET => '', + self::XHTML1_RDFA => '', + self::XHTML1_RDFA11 => '', + self::XHTML_BASIC1 => '', + self::XHTML5 => '', + self::HTML4_STRICT => '', + self::HTML4_LOOSE => '', + self::HTML4_FRAMESET => '', + self::HTML5 => '', + ), + )); + } + + /** + * Unset the static doctype registry + * + * Mainly useful for testing purposes. Sets {@link $registeredDoctypes} to + * a null value. + * + * @return void + */ + public static function unsetDoctypeRegistry() + { + static::$registeredDoctypes = null; + } + + /** + * Constructor + * + * Map constants to doctype strings, and set default doctype + */ + public function __construct() + { + if (null === static::$registeredDoctypes) { + static::registerDefaultDoctypes(); + $this->setDoctype($this->defaultDoctype); + } + $this->registry = static::$registeredDoctypes; + } + + /** + * Set or retrieve doctype + * + * @param string $doctype + * @return Doctype Provides a fluent interface + * @throws Exception\DomainException + */ + public function __invoke($doctype = null) + { + if (null !== $doctype) { + switch ($doctype) { + case self::XHTML11: + case self::XHTML1_STRICT: + case self::XHTML1_TRANSITIONAL: + case self::XHTML1_FRAMESET: + case self::XHTML_BASIC1: + case self::XHTML1_RDFA: + case self::XHTML1_RDFA11: + case self::XHTML5: + case self::HTML4_STRICT: + case self::HTML4_LOOSE: + case self::HTML4_FRAMESET: + case self::HTML5: + $this->setDoctype($doctype); + break; + default: + if (substr($doctype, 0, 9) != 'setDoctype($type); + $this->registry['doctypes'][$type] = $doctype; + break; + } + } + + return $this; + } + + /** + * Set doctype + * + * @param string $doctype + * @return Doctype + */ + public function setDoctype($doctype) + { + $this->registry['doctype'] = $doctype; + return $this; + } + + /** + * Retrieve doctype + * + * @return string + */ + public function getDoctype() + { + if (!isset($this->registry['doctype'])) { + $this->setDoctype($this->defaultDoctype); + } + return $this->registry['doctype']; + } + + /** + * Get doctype => string mappings + * + * @return array + */ + public function getDoctypes() + { + return $this->registry['doctypes']; + } + + /** + * Is doctype XHTML? + * + * @return boolean + */ + public function isXhtml() + { + return (stristr($this->getDoctype(), 'xhtml') ? true : false); + } + + /** + * Is doctype HTML5? (HeadMeta uses this for validation) + * + * @return boolean + */ + public function isHtml5() + { + return (stristr($this->__invoke(), '') ? true : false); + } + + /** + * Is doctype RDFa? + * + * @return boolean + */ + public function isRdfa() + { + return ($this->isHtml5() || stristr($this->getDoctype(), 'rdfa') ? true : false); + } + + /** + * String representation of doctype + * + * @return string + */ + public function __toString() + { + $doctypes = $this->getDoctypes(); + return $doctypes[$this->getDoctype()]; + } +} diff --git a/src/Helper/EscapeCss.php b/src/Helper/EscapeCss.php new file mode 100644 index 00000000..597d4486 --- /dev/null +++ b/src/Helper/EscapeCss.php @@ -0,0 +1,36 @@ +getEscaper()->escapeCss($value); + } + +} diff --git a/src/Helper/EscapeHtml.php b/src/Helper/EscapeHtml.php new file mode 100644 index 00000000..0ee00dcc --- /dev/null +++ b/src/Helper/EscapeHtml.php @@ -0,0 +1,36 @@ +getEscaper()->escapeHtml($value); + } + +} diff --git a/src/Helper/EscapeHtmlAttr.php b/src/Helper/EscapeHtmlAttr.php new file mode 100644 index 00000000..e674f089 --- /dev/null +++ b/src/Helper/EscapeHtmlAttr.php @@ -0,0 +1,36 @@ +getEscaper()->escapeHtmlAttr($value); + } + +} diff --git a/src/Helper/EscapeJs.php b/src/Helper/EscapeJs.php new file mode 100644 index 00000000..c186e109 --- /dev/null +++ b/src/Helper/EscapeJs.php @@ -0,0 +1,36 @@ +getEscaper()->escapeJs($value); + } + +} diff --git a/src/Helper/EscapeUrl.php b/src/Helper/EscapeUrl.php new file mode 100644 index 00000000..a687f133 --- /dev/null +++ b/src/Helper/EscapeUrl.php @@ -0,0 +1,36 @@ +getEscaper()->escapeUrl($value); + } + +} diff --git a/src/Helper/Escaper/AbstractHelper.php b/src/Helper/Escaper/AbstractHelper.php new file mode 100644 index 00000000..453cbb50 --- /dev/null +++ b/src/Helper/Escaper/AbstractHelper.php @@ -0,0 +1,138 @@ +escaper = $escaper; + $this->encoding = $escaper->getEncoding(); + return $this; + } + + public function getEscaper() + { + if (null === $this->escaper) { + $this->setEscaper(new Escaper\Escaper($this->getEncoding())); + } + return $this->escaper; + } + + /** + * Set the encoding to use for escape operations + * + * @param string $encoding + * @return AbstractEscaper + */ + public function setEncoding($encoding) + { + if (!is_null($this->escaper)) { + throw new Exception\InvalidArgumentException( + 'Character encoding settings cannot be changed once the Helper has been used or ' + . ' if a Zend\Escaper\Escaper object (with preset encoding option) is set.' + ); + } + $this->encoding = $encoding; + return $this; + } + + /** + * Get the encoding to use for escape operations + * + * @return string + */ + public function getEncoding() + { + return $this->encoding; + } + + /** + * Invoke this helper: escape a value + * + * @param mixed $value + * @param int $recurse Expects one of the recursion constants; used to decide whether or not to recurse the given value when escaping + * @return mixed Given a scalar, a scalar value is returned. Given an object, with the $recurse flag not allowing object recursion, returns a string. Otherwise, returns an array. + * @throws Exception\InvalidArgumentException + */ + public function __invoke($value, $recurse = self::RECURSE_NONE) + { + if (is_string($value)) { + return $this->escape($value); + } + if (is_array($value)) { + if (!(self::RECURSE_ARRAY & $recurse)) { + throw new Exception\InvalidArgumentException( + 'Array provided to Escape helper, but flags do not allow recursion' + ); + } + foreach ($value as $k => $v) { + $value[$k] = $this->__invoke($v, $recurse); + } + return $value; + } + if (is_object($value)) { + if (!(self::RECURSE_OBJECT & $recurse)) { + // Attempt to cast it to a string + if (method_exists($value, '__toString')) { + return $this->escape((string) $value); + } + throw new Exception\InvalidArgumentException( + 'Object provided to Escape helper, but flags do not allow recursion' + ); + } + if (method_exists($value, 'toArray')) { + return $this->__invoke($value->toArray(), $recurse | self::RECURSE_ARRAY); + } + return $this->__invoke((array) $value, $recurse | self::RECURSE_ARRAY); + } + // At this point, we have a scalar; simply return it + return $value; + } + + /** + * Escape a value for current escaping strategy + * + * @param string $value + * @return string + */ + abstract protected function escape($value); + +} diff --git a/src/Helper/Gravatar.php b/src/Helper/Gravatar.php new file mode 100644 index 00000000..f15256ec --- /dev/null +++ b/src/Helper/Gravatar.php @@ -0,0 +1,351 @@ + 80, + 'default_img' => self::DEFAULT_MM, + 'rating' => self::RATING_G, + 'secure' => null, + ); + + /** + * Email Address + * + * @var string + */ + protected $email; + + /** + * Attributes for HTML image tag + * + * @var array + */ + protected $attribs; + + /** + * Returns an avatar from gravatar's service. + * + * $options may include the following: + * - 'img_size' int height of img to return + * - 'default_img' string img to return if email address has not found + * - 'rating' string rating parameter for avatar + * - 'secure' bool load from the SSL or Non-SSL location + * + * @see http://pl.gravatar.com/site/implement/url + * @see http://pl.gravatar.com/site/implement/url More information about gravatar's service. + * @param string|null $email Email address. + * @param null|array $options Options + * @param array $attribs Attributes for image tag (title, alt etc.) + * @return Gravatar + */ + public function __invoke($email = "", $options = array(), $attribs = array()) + { + if (!empty($email)) { + $this->setEmail($email); + } + if (!empty($options)) { + $this->setOptions($options); + } + if (!empty($attribs)) { + $this->setAttribs($attribs); + } + return $this; + } + + /** + * Configure state + * + * @param array $options + * @return Gravatar + */ + public function setOptions(array $options) + { + foreach ($options as $key => $value) { + $method = 'set' . str_replace(' ', '', ucwords(str_replace('_', ' ', $key))); + if (method_exists($this, $method)) { + $this->{$method}($value); + } + } + return $this; + } + + /** + * Get img size + * + * @return int The img size + */ + public function getImgSize() + { + return $this->options['img_size']; + } + + /** + * Set img size in pixels + * + * @param int $imgSize Size of img must be between 1 and 512 + * @return Gravatar + */ + public function setImgSize($imgSize) + { + $this->options['img_size'] = (int) $imgSize; + return $this; + } + + /** + * Get default img + * + * @return string + */ + public function getDefaultImg() + { + return $this->options['default_img']; + } + + /** + * Set default img + * + * Can be either an absolute URL to an image, or one of the DEFAULT_* constants + * + * @link http://pl.gravatar.com/site/implement/url More information about default image. + * @param string $defaultImg + * @return Gravatar + */ + public function setDefaultImg($defaultImg) + { + $this->options['default_img'] = urlencode($defaultImg); + return $this; + } + + /** + * Set rating value + * + * Must be one of the RATING_* constants + * + * @link http://pl.gravatar.com/site/implement/url More information about rating. + * @param string $rating Value for rating. Allowed values are: g, px, r,x + * @return Gravatar + * @throws Exception\DomainException + */ + public function setRating($rating) + { + switch ($rating) { + case self::RATING_G: + case self::RATING_PG: + case self::RATING_R: + case self::RATING_X: + $this->options['rating'] = $rating; + break; + default: + throw new Exception\DomainException(sprintf( + 'The rating value "%s" is not allowed', + $rating + )); + } + return $this; + } + + /** + * Get rating value + * + * @return string + */ + public function getRating() + { + return $this->options['rating']; + } + + /** + * Set email address + * + * @param string $email + * @return Gravatar + */ + public function setEmail( $email ) + { + $this->email = $email; + return $this; + } + + /** + * Get email address + * + * @return string + */ + public function getEmail() + { + return $this->email; + } + + /** + * Load from an SSL or No-SSL location? + * + * @param bool $flag + * @return Gravatar + */ + public function setSecure($flag) + { + $this->options['secure'] = ($flag === null) ? null : (bool) $flag; + return $this; + } + + /** + * Get an SSL or a No-SSL location + * + * @return bool + */ + public function getSecure() + { + if ($this->options['secure'] === null) { + return (isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off'); + } + return $this->options['secure']; + } + + /** + * Get attribs of image + * + * Warning! + * If you set src attrib, you get it, but this value will be overwritten in + * protected method setSrcAttribForImg(). And finally your get other src + * value! + * + * @return array + */ + public function getAttribs() + { + return $this->attribs; + } + + /** + * Set attribs for image tag + * + * Warning! You shouldn't set src attrib for image tag. + * This attrib is overwritten in protected method setSrcAttribForImg(). + * This method(_setSrcAttribForImg) is called in public method getImgTag(). + * + * @param array $attribs + * @return Gravatar + */ + public function setAttribs(array $attribs) + { + $this->attribs = $attribs; + return $this; + } + + /** + * Get URL to gravatar's service. + * + * @return string URL + */ + protected function getGravatarUrl() + { + return ($this->getSecure() === false) ? self::GRAVATAR_URL : self::GRAVATAR_URL_SECURE; + } + + /** + * Get avatar url (including size, rating and default image options) + * + * @return string + */ + protected function getAvatarUrl() + { + $src = $this->getGravatarUrl() + . '/' . md5($this->getEmail()) + . '?s=' . $this->getImgSize() + . '&d=' . $this->getDefaultImg() + . '&r=' . $this->getRating(); + return $src; + } + + /** + * Set src attrib for image. + * + * You shouldn't set a own url value! + * It sets value, uses protected method getAvatarUrl. + * + * If already exists, it will be overwritten. + * + * @return void + */ + protected function setSrcAttribForImg() + { + $attribs = $this->getAttribs(); + $attribs['src'] = $this->getAvatarUrl(); + $this->setAttribs($attribs); + } + + /** + * Return valid image tag + * + * @return string + */ + public function getImgTag() + { + $this->setSrcAttribForImg(); + $html = 'htmlAttribs($this->getAttribs()) + . $this->getClosingBracket(); + + return $html; + } + + /** + * Return valid image tag + * + * @return string + */ + public function __toString() + { + return $this->getImgTag(); + } +} diff --git a/src/Helper/HeadLink.php b/src/Helper/HeadLink.php new file mode 100644 index 00000000..0fcc1371 --- /dev/null +++ b/src/Helper/HeadLink.php @@ -0,0 +1,425 @@ +setSeparator(PHP_EOL); + } + + /** + * headLink() - View Helper Method + * + * Returns current object instance. Optionally, allows passing array of + * values to build link. + * + * @return \Zend\View\Helper\HeadLink + */ + public function __invoke(array $attributes = null, $placement = Placeholder\Container\AbstractContainer::APPEND) + { + if (null !== $attributes) { + $item = $this->createData($attributes); + switch ($placement) { + case Placeholder\Container\AbstractContainer::SET: + $this->set($item); + break; + case Placeholder\Container\AbstractContainer::PREPEND: + $this->prepend($item); + break; + case Placeholder\Container\AbstractContainer::APPEND: + default: + $this->append($item); + break; + } + } + return $this; + } + + /** + * Overload method access + * + * Creates the following virtual methods: + * - appendStylesheet($href, $media, $conditionalStylesheet, $extras) + * - offsetSetStylesheet($index, $href, $media, $conditionalStylesheet, $extras) + * - prependStylesheet($href, $media, $conditionalStylesheet, $extras) + * - setStylesheet($href, $media, $conditionalStylesheet, $extras) + * - appendAlternate($href, $type, $title, $extras) + * - offsetSetAlternate($index, $href, $type, $title, $extras) + * - prependAlternate($href, $type, $title, $extras) + * - setAlternate($href, $type, $title, $extras) + * + * Items that may be added in the future: + * - Navigation? need to find docs on this + * - public function appendStart() + * - public function appendContents() + * - public function appendPrev() + * - public function appendNext() + * - public function appendIndex() + * - public function appendEnd() + * - public function appendGlossary() + * - public function appendAppendix() + * - public function appendHelp() + * - public function appendBookmark() + * - Other? + * - public function appendCopyright() + * - public function appendChapter() + * - public function appendSection() + * - public function appendSubsection() + * + * @param mixed $method + * @param mixed $args + * @return void + * @throws Exception\BadMethodCallException + */ + public function __call($method, $args) + { + if (preg_match('/^(?Pset|(ap|pre)pend|offsetSet)(?PStylesheet|Alternate)$/', $method, $matches)) { + $argc = count($args); + $action = $matches['action']; + $type = $matches['type']; + $index = null; + + if ('offsetSet' == $action) { + if (0 < $argc) { + $index = array_shift($args); + --$argc; + } + } + + if (1 > $argc) { + throw new Exception\BadMethodCallException(sprintf( + '%s requires at least one argument', + $method + )); + } + + if (is_array($args[0])) { + $item = $this->createData($args[0]); + } else { + $dataMethod = 'createData' . $type; + $item = $this->$dataMethod($args); + } + + if ($item) { + if ('offsetSet' == $action) { + $this->offsetSet($index, $item); + } else { + $this->$action($item); + } + } + + return $this; + } + + return parent::__call($method, $args); + } + + /** + * Check if value is valid + * + * @param mixed $value + * @return boolean + */ + protected function isValid($value) + { + if (!$value instanceof \stdClass) { + return false; + } + + $vars = get_object_vars($value); + $keys = array_keys($vars); + $intersection = array_intersect($this->itemKeys, $keys); + if (empty($intersection)) { + return false; + } + + return true; + } + + /** + * append() + * + * @param array $value + * @return void + * @throws Exception\InvalidArgumentException + */ + public function append($value) + { + if (!$this->isValid($value)) { + throw new Exception\InvalidArgumentException( + 'append() expects a data token; please use one of the custom append*() methods' + ); + } + + return $this->getContainer()->append($value); + } + + /** + * offsetSet() + * + * @param string|int $index + * @param array $value + * @return void + * @throws Exception\InvalidArgumentException + */ + public function offsetSet($index, $value) + { + if (!$this->isValid($value)) { + throw new Exception\InvalidArgumentException( + 'offsetSet() expects a data token; please use one of the custom offsetSet*() methods' + ); + } + + return $this->getContainer()->offsetSet($index, $value); + } + + /** + * prepend() + * + * @param array $value + * @return Zend_Layout_ViewHelper_HeadLink + * @throws Exception\InvalidArgumentException + */ + public function prepend($value) + { + if (!$this->isValid($value)) { + throw new Exception\InvalidArgumentException( + 'prepend() expects a data token; please use one of the custom prepend*() methods' + ); + } + + return $this->getContainer()->prepend($value); + } + + /** + * set() + * + * @param array $value + * @return Zend_Layout_ViewHelper_HeadLink + * @throws Exception\InvalidArgumentException + */ + public function set($value) + { + if (!$this->isValid($value)) { + throw new Exception\InvalidArgumentException( + 'set() expects a data token; please use one of the custom set*() methods' + ); + } + + return $this->getContainer()->set($value); + } + + + /** + * Create HTML link element from data item + * + * @param stdClass $item + * @return string + */ + public function itemToString(\stdClass $item) + { + $attributes = (array) $item; + $link = 'itemKeys as $itemKey) { + if (isset($attributes[$itemKey])) { + if (is_array($attributes[$itemKey])) { + foreach ($attributes[$itemKey] as $key => $value) { + $link .= sprintf('%s="%s" ', $key, ($this->autoEscape) ? $this->escape($value) : $value); + } + } else { + $link .= sprintf('%s="%s" ', $itemKey, ($this->autoEscape) ? $this->escape($attributes[$itemKey]) : $attributes[$itemKey]); + } + } + } + + if (method_exists($this->view, 'plugin')) { + $link .= ($this->view->plugin('doctype')->isXhtml()) ? '/>' : '>'; + } else { + $link .= '/>'; + } + + if (($link == '') || ($link == '')) { + return ''; + } + + if (isset($attributes['conditionalStylesheet']) + && !empty($attributes['conditionalStylesheet']) + && is_string($attributes['conditionalStylesheet'])) + { + $link = ''; + } + + return $link; + } + + /** + * Render link elements as string + * + * @param string|int $indent + * @return string + */ + public function toString($indent = null) + { + $indent = (null !== $indent) + ? $this->getWhitespace($indent) + : $this->getIndent(); + + $items = array(); + $this->getContainer()->ksort(); + foreach ($this as $item) { + $items[] = $this->itemToString($item); + } + + return $indent . implode($this->escape($this->getSeparator()) . $indent, $items); + } + + /** + * Create data item for stack + * + * @param array $attributes + * @return stdClass + */ + public function createData(array $attributes) + { + $data = (object) $attributes; + return $data; + } + + /** + * Create item for stylesheet link item + * + * @param array $args + * @return stdClass|false Returns false if stylesheet is a duplicate + */ + public function createDataStylesheet(array $args) + { + $rel = 'stylesheet'; + $type = 'text/css'; + $media = 'screen'; + $conditionalStylesheet = false; + $href = array_shift($args); + + if ($this->isDuplicateStylesheet($href)) { + return false; + } + + if (0 < count($args)) { + $media = array_shift($args); + if (is_array($media)) { + $media = implode(',', $media); + } else { + $media = (string) $media; + } + } + if (0 < count($args)) { + $conditionalStylesheet = array_shift($args); + if (!empty($conditionalStylesheet) && is_string($conditionalStylesheet)) { + $conditionalStylesheet = (string) $conditionalStylesheet; + } else { + $conditionalStylesheet = null; + } + } + + if (0 < count($args) && is_array($args[0])) { + $extras = array_shift($args); + $extras = (array) $extras; + } + + $attributes = compact('rel', 'type', 'href', 'media', 'conditionalStylesheet', 'extras'); + return $this->createData($attributes); + } + + /** + * Is the linked stylesheet a duplicate? + * + * @param string $uri + * @return bool + */ + protected function isDuplicateStylesheet($uri) + { + foreach ($this->getContainer() as $item) { + if (($item->rel == 'stylesheet') && ($item->href == $uri)) { + return true; + } + } + return false; + } + + /** + * Create item for alternate link item + * + * @param array $args + * @return stdClass + * @throws Exception\InvalidArgumentException + */ + public function createDataAlternate(array $args) + { + if (3 > count($args)) { + throw new Exception\InvalidArgumentException(sprintf( + 'Alternate tags require 3 arguments; %s provided', + count($args) + )); + } + + $rel = 'alternate'; + $href = array_shift($args); + $type = array_shift($args); + $title = array_shift($args); + + if (0 < count($args) && is_array($args[0])) { + $extras = array_shift($args); + $extras = (array) $extras; + + if (isset($extras['media']) && is_array($extras['media'])) { + $extras['media'] = implode(',', $extras['media']); + } + } + + $href = (string) $href; + $type = (string) $type; + $title = (string) $title; + + $attributes = compact('rel', 'href', 'type', 'title', 'extras'); + return $this->createData($attributes); + } +} diff --git a/src/Helper/HeadMeta.php b/src/Helper/HeadMeta.php new file mode 100644 index 00000000..bc6e1acb --- /dev/null +++ b/src/Helper/HeadMeta.php @@ -0,0 +1,419 @@ +setSeparator(PHP_EOL); + } + + /** + * Retrieve object instance; optionally add meta tag + * + * @param string $content + * @param string $keyValue + * @param string $keyType + * @param array $modifiers + * @param string $placement + * @return \Zend\View\Helper\HeadMeta + */ + public function __invoke($content = null, $keyValue = null, $keyType = 'name', $modifiers = array(), $placement = Placeholder\Container\AbstractContainer::APPEND) + { + if ((null !== $content) && (null !== $keyValue)) { + $item = $this->createData($keyType, $keyValue, $content, $modifiers); + $action = strtolower($placement); + switch ($action) { + case 'append': + case 'prepend': + case 'set': + $this->$action($item); + break; + default: + $this->append($item); + break; + } + } + + return $this; + } + + /** + * Normalize type attribute of meta + * + * @param $type type in CamelCase + * @return string + * @throws Exception\DomainException + */ + protected function normalizeType($type) + { + switch ($type) { + case 'Name': + return 'name'; + case 'HttpEquiv': + return 'http-equiv'; + case 'Property': + return 'property'; + default: + throw new Exception\DomainException(sprintf( + 'Invalid type "%s" passed to normalizeType', + $type + )); + } + } + + /** + * Overload method access + * + * Allows the following 'virtual' methods: + * - appendName($keyValue, $content, $modifiers = array()) + * - offsetGetName($index, $keyValue, $content, $modifiers = array()) + * - prependName($keyValue, $content, $modifiers = array()) + * - setName($keyValue, $content, $modifiers = array()) + * - appendHttpEquiv($keyValue, $content, $modifiers = array()) + * - offsetGetHttpEquiv($index, $keyValue, $content, $modifiers = array()) + * - prependHttpEquiv($keyValue, $content, $modifiers = array()) + * - setHttpEquiv($keyValue, $content, $modifiers = array()) + * - appendProperty($keyValue, $content, $modifiers = array()) + * - offsetGetProperty($index, $keyValue, $content, $modifiers = array()) + * - prependProperty($keyValue, $content, $modifiers = array()) + * - setProperty($keyValue, $content, $modifiers = array()) + * + * @param string $method + * @param array $args + * @return \Zend\View\Helper\HeadMeta + * @throws Exception\BadMethodCallException + */ + public function __call($method, $args) + { + if (preg_match('/^(?Pset|(pre|ap)pend|offsetSet)(?PName|HttpEquiv|Property)$/', $method, $matches)) { + $action = $matches['action']; + $type = $this->normalizeType($matches['type']); + $argc = count($args); + $index = null; + + if ('offsetSet' == $action) { + if (0 < $argc) { + $index = array_shift($args); + --$argc; + } + } + + if (2 > $argc) { + throw new Exception\BadMethodCallException( + 'Too few arguments provided; requires key value, and content' + ); + } + + if (3 > $argc) { + $args[] = array(); + } + + $item = $this->createData($type, $args[0], $args[1], $args[2]); + + if ('offsetSet' == $action) { + return $this->offsetSet($index, $item); + } + + $this->$action($item); + return $this; + } + + return parent::__call($method, $args); + } + + /** + * Create an HTML5-style meta charset tag. Something like + * + * Not valid in a non-HTML5 doctype + * + * @param string $charset + * @return \Zend\View\Helper\HeadMeta Provides a fluent interface + */ + public function setCharset($charset) + { + $item = new \stdClass; + $item->type = 'charset'; + $item->charset = $charset; + $item->content = null; + $item->modifiers = array(); + $this->set($item); + return $this; + } + + /** + * Determine if item is valid + * + * @param mixed $item + * @return boolean + */ + protected function isValid($item) + { + if ((!$item instanceof \stdClass) + || !isset($item->type) + || !isset($item->modifiers)) + { + return false; + } + + if (!isset($item->content) + && (! $this->view->plugin('doctype')->isHtml5() + || (! $this->view->plugin('doctype')->isHtml5() && $item->type !== 'charset'))) { + return false; + } + + // is only supported with doctype RDFa + if (!$this->view->plugin('doctype')->isRdfa() + && $item->type === 'property') { + return false; + } + + return true; + } + + /** + * Append + * + * @param string $value + * @return void + * @throws Exception\InvalidArgumentException + */ + public function append($value) + { + if (!$this->isValid($value)) { + throw new Exception\InvalidArgumentException( + 'Invalid value passed to append; please use appendMeta()' + ); + } + + return $this->getContainer()->append($value); + } + + /** + * OffsetSet + * + * @param string|int $index + * @param string $value + * @return void + * @throws Exception\InvalidArgumentException + */ + public function offsetSet($index, $value) + { + if (!$this->isValid($value)) { + throw new Exception\InvalidArgumentException( + 'Invalid value passed to offsetSet; please use offsetSetName() or offsetSetHttpEquiv()' + ); + } + + return $this->getContainer()->offsetSet($index, $value); + } + + /** + * OffsetUnset + * + * @param string|int $index + * @return void + * @throws Exception\InvalidArgumentException + */ + public function offsetUnset($index) + { + if (!in_array($index, $this->getContainer()->getKeys())) { + throw new Exception\InvalidArgumentException('Invalid index passed to offsetUnset()'); + } + + return $this->getContainer()->offsetUnset($index); + } + + /** + * Prepend + * + * @param string $value + * @return void + * @throws Exception\InvalidArgumentException + */ + public function prepend($value) + { + if (!$this->isValid($value)) { + throw new Exception\InvalidArgumentException( + 'Invalid value passed to prepend; please use prependMeta()' + ); + } + + return $this->getContainer()->prepend($value); + } + + /** + * Set + * + * @param string $value + * @return void + * @throws Exception\InvalidArgumentException + */ + public function set($value) + { + if (!$this->isValid($value)) { + throw new Exception\InvalidArgumentException('Invalid value passed to set; please use setMeta()'); + } + + $container = $this->getContainer(); + foreach ($container->getArrayCopy() as $index => $item) { + if ($item->type == $value->type && $item->{$item->type} == $value->{$value->type}) { + $this->offsetUnset($index); + } + } + + return $this->append($value); + } + + /** + * Build meta HTML string + * + * @param string $type + * @param string $typeValue + * @param string $content + * @param array $modifiers + * @return string + * @throws Exception\InvalidArgumentException + */ + public function itemToString(\stdClass $item) + { + if (!in_array($item->type, $this->typeKeys)) { + throw new Exception\InvalidArgumentException(sprintf( + 'Invalid type "%s" provided for meta', + $item->type + )); + } + $type = $item->type; + + $modifiersString = ''; + foreach ($item->modifiers as $key => $value) { + if ($this->view->plugin('doctype')->isHtml5() + && $key == 'scheme' + ) { + throw new Exception\InvalidArgumentException( + 'Invalid modifier "scheme" provided; not supported by HTML5' + ); + } + if (!in_array($key, $this->modifierKeys)) { + continue; + } + $modifiersString .= $key . '="' . $this->escape($value) . '" '; + } + + if (method_exists($this->view, 'plugin')) { + if ($this->view->plugin('doctype')->isHtml5() + && $type == 'charset' + ) { + $tpl = ($this->view->plugin('doctype')->isXhtml()) + ? '' + : ''; + } elseif ($this->view->plugin('doctype')->isXhtml()) { + $tpl = ''; + } else { + $tpl = ''; + } + } else { + $tpl = ''; + } + + $meta = sprintf( + $tpl, + $type, + $this->escape($item->$type), + $this->escape($item->content), + $modifiersString + ); + + if (isset($item->modifiers['conditional']) + && !empty($item->modifiers['conditional']) + && is_string($item->modifiers['conditional'])) + { + $meta = ''; + } + + return $meta; + } + + /** + * Render placeholder as string + * + * @param string|int $indent + * @return string + */ + public function toString($indent = null) + { + $indent = (null !== $indent) + ? $this->getWhitespace($indent) + : $this->getIndent(); + + $items = array(); + $this->getContainer()->ksort(); + try { + foreach ($this as $item) { + $items[] = $this->itemToString($item); + } + } catch (Exception\InvalidArgumentException $e) { + trigger_error($e->getMessage(), E_USER_WARNING); + return ''; + } + return $indent . implode($this->escape($this->getSeparator()) . $indent, $items); + } + + /** + * Create data item for inserting into stack + * + * @param string $type + * @param string $typeValue + * @param string $content + * @param array $modifiers + * @return stdClass + */ + public function createData($type, $typeValue, $content, array $modifiers) + { + $data = new \stdClass; + $data->type = $type; + $data->$type = $typeValue; + $data->content = $content; + $data->modifiers = $modifiers; + return $data; + } +} diff --git a/src/Helper/HeadScript.php b/src/Helper/HeadScript.php new file mode 100644 index 00000000..12db060c --- /dev/null +++ b/src/Helper/HeadScript.php @@ -0,0 +1,494 @@ +setSeparator(PHP_EOL); + } + + /** + * Return headScript object + * + * Returns headScript helper object; optionally, allows specifying a script + * or script file to include. + * + * @param string $mode Script or file + * @param string $spec Script/url + * @param string $placement Append, prepend, or set + * @param array $attrs Array of script attributes + * @param string $type Script type and/or array of script attributes + * @return \Zend\View\Helper\HeadScript + */ + public function __invoke($mode = HeadScript::FILE, $spec = null, $placement = 'APPEND', array $attrs = array(), $type = 'text/javascript') + { + if ((null !== $spec) && is_string($spec)) { + $action = ucfirst(strtolower($mode)); + $placement = strtolower($placement); + switch ($placement) { + case 'set': + case 'prepend': + case 'append': + $action = $placement . $action; + break; + default: + $action = 'append' . $action; + break; + } + $this->$action($spec, $type, $attrs); + } + + return $this; + } + + /** + * Start capture action + * + * @param mixed $captureType Type of capture + * @param string $type Type of script + * @param array $attrs Attributes of capture + * @return void + * @throws Exception\RuntimeException + */ + public function captureStart($captureType = Placeholder\Container\AbstractContainer::APPEND, $type = 'text/javascript', $attrs = array()) + { + if ($this->captureLock) { + throw new Exception\RuntimeException('Cannot nest headScript captures'); + } + + $this->captureLock = true; + $this->captureType = $captureType; + $this->captureScriptType = $type; + $this->captureScriptAttrs = $attrs; + ob_start(); + } + + /** + * End capture action and store + * + * @return void + */ + public function captureEnd() + { + $content = ob_get_clean(); + $type = $this->captureScriptType; + $attrs = $this->captureScriptAttrs; + $this->captureScriptType = null; + $this->captureScriptAttrs = null; + $this->captureLock = false; + + switch ($this->captureType) { + case Placeholder\Container\AbstractContainer::SET: + case Placeholder\Container\AbstractContainer::PREPEND: + case Placeholder\Container\AbstractContainer::APPEND: + $action = strtolower($this->captureType) . 'Script'; + break; + default: + $action = 'appendScript'; + break; + } + $this->$action($content, $type, $attrs); + } + + /** + * Overload method access + * + * Allows the following method calls: + * - appendFile($src, $type = 'text/javascript', $attrs = array()) + * - offsetSetFile($index, $src, $type = 'text/javascript', $attrs = array()) + * - prependFile($src, $type = 'text/javascript', $attrs = array()) + * - setFile($src, $type = 'text/javascript', $attrs = array()) + * - appendScript($script, $type = 'text/javascript', $attrs = array()) + * - offsetSetScript($index, $src, $type = 'text/javascript', $attrs = array()) + * - prependScript($script, $type = 'text/javascript', $attrs = array()) + * - setScript($script, $type = 'text/javascript', $attrs = array()) + * + * @param string $method Method to call + * @param array $args Arguments of method + * @return \Zend\View\Helper\HeadScript + * @throws Exception\BadMethodCallException if too few arguments or invalid method + */ + public function __call($method, $args) + { + if (preg_match('/^(?Pset|(ap|pre)pend|offsetSet)(?PFile|Script)$/', $method, $matches)) { + if (1 > count($args)) { + throw new Exception\BadMethodCallException(sprintf( + 'Method "%s" requires at least one argument', + $method + )); + } + + $action = $matches['action']; + $mode = strtolower($matches['mode']); + $type = 'text/javascript'; + $attrs = array(); + + if ('offsetSet' == $action) { + $index = array_shift($args); + if (1 > count($args)) { + throw new Exception\BadMethodCallException(sprintf( + 'Method "%s" requires at least two arguments, an index and source', + $method + )); + } + } + + $content = $args[0]; + + if (isset($args[1])) { + $type = (string) $args[1]; + } + if (isset($args[2])) { + $attrs = (array) $args[2]; + } + + switch ($mode) { + case 'script': + $item = $this->createData($type, $attrs, $content); + if ('offsetSet' == $action) { + $this->offsetSet($index, $item); + } else { + $this->$action($item); + } + break; + case 'file': + default: + if (!$this->isDuplicate($content)) { + $attrs['src'] = $content; + $item = $this->createData($type, $attrs); + if ('offsetSet' == $action) { + $this->offsetSet($index, $item); + } else { + $this->$action($item); + } + } + break; + } + + return $this; + } + + return parent::__call($method, $args); + } + + /** + * Is the file specified a duplicate? + * + * @param string $file Name of file to check + * @return bool + */ + protected function isDuplicate($file) + { + foreach ($this->getContainer() as $item) { + if (($item->source === null) + && array_key_exists('src', $item->attributes) + && ($file == $item->attributes['src'])) + { + return true; + } + } + return false; + } + + /** + * Is the script provided valid? + * + * @param mixed $value Is the given script valid? + * @return bool + */ + protected function isValid($value) + { + if ((!$value instanceof \stdClass) + || !isset($value->type) + || (!isset($value->source) && !isset($value->attributes))) + { + return false; + } + + return true; + } + + /** + * Override append + * + * @param string $value Append script or file + * @return void + * @throws Exception\InvalidArgumentException + */ + public function append($value) + { + if (!$this->isValid($value)) { + throw new Exception\InvalidArgumentException( + 'Invalid argument passed to append(); please use one of the helper methods, appendScript() or appendFile()' + ); + } + + return $this->getContainer()->append($value); + } + + /** + * Override prepend + * + * @param string $value Prepend script or file + * @return void + * @throws Exception\InvalidArgumentException + */ + public function prepend($value) + { + if (!$this->isValid($value)) { + throw new Exception\InvalidArgumentException( + 'Invalid argument passed to prepend(); please use one of the helper methods, prependScript() or prependFile()' + ); + } + + return $this->getContainer()->prepend($value); + } + + /** + * Override set + * + * @param string $value Set script or file + * @return void + * @throws Exception\InvalidArgumentException + */ + public function set($value) + { + if (!$this->isValid($value)) { + throw new Exception\InvalidArgumentException( + 'Invalid argument passed to set(); please use one of the helper methods, setScript() or setFile()' + ); + } + + return $this->getContainer()->set($value); + } + + /** + * Override offsetSet + * + * @param string|int $index Set script of file offset + * @param mixed $value + * @return void + * @throws Exception\InvalidArgumentException + */ + public function offsetSet($index, $value) + { + if (!$this->isValid($value)) { + throw new Exception\InvalidArgumentException( + 'Invalid argument passed to offsetSet(); please use one of the helper methods, offsetSetScript() or offsetSetFile()' + ); + } + + return $this->getContainer()->offsetSet($index, $value); + } + + /** + * Set flag indicating if arbitrary attributes are allowed + * + * @param bool $flag Set flag + * @return \Zend\View\Helper\HeadScript + */ + public function setAllowArbitraryAttributes($flag) + { + $this->arbitraryAttributes = (bool) $flag; + return $this; + } + + /** + * Are arbitrary attributes allowed? + * + * @return bool + */ + public function arbitraryAttributesAllowed() + { + return $this->arbitraryAttributes; + } + + /** + * Create script HTML + * + * @param mixed $item Item to convert + * @param string $indent String to add before the item + * @param string $escapeStart Starting sequence + * @param string $escapeEnd Ending sequence + * @return string + */ + public function itemToString($item, $indent, $escapeStart, $escapeEnd) + { + $attrString = ''; + if (!empty($item->attributes)) { + foreach ($item->attributes as $key => $value) { + if ((!$this->arbitraryAttributesAllowed() && !in_array($key, $this->optionalAttributes)) + || in_array($key, array('conditional', 'noescape'))) + { + continue; + } + if ('defer' == $key) { + $value = 'defer'; + } + $attrString .= sprintf(' %s="%s"', $key, ($this->autoEscape) ? $this->escape($value) : $value); + } + } + + + $addScriptEscape = !(isset($item->attributes['noescape']) && filter_var($item->attributes['noescape'], FILTER_VALIDATE_BOOLEAN)); + + $type = ($this->autoEscape) ? $this->escape($item->type) : $item->type; + $html = ''; + + if (isset($item->attributes['conditional']) + && !empty($item->attributes['conditional']) + && is_string($item->attributes['conditional'])) + { + $html = $indent . ''; + } else { + $html = $indent . $html; + } + + return $html; + } + + /** + * Retrieve string representation + * + * @param string|int $indent Amount of whitespaces or string to use for indention + * @return string + */ + public function toString($indent = null) + { + $indent = (null !== $indent) + ? $this->getWhitespace($indent) + : $this->getIndent(); + + if ($this->view) { + $useCdata = $this->view->plugin('doctype')->isXhtml() ? true : false; + } else { + $useCdata = $this->useCdata ? true : false; + } + + $escapeStart = ($useCdata) ? '//' : '//-->'; + + $items = array(); + $this->getContainer()->ksort(); + foreach ($this as $item) { + if (!$this->isValid($item)) { + continue; + } + + $items[] = $this->itemToString($item, $indent, $escapeStart, $escapeEnd); + } + + $return = implode($this->getSeparator(), $items); + return $return; + } + + /** + * Create data item containing all necessary components of script + * + * @param string $type Type of data + * @param array $attributes Attributes of data + * @param string $content Content of data + * @return stdClass + */ + public function createData($type, array $attributes, $content = null) + { + $data = new \stdClass(); + $data->type = $type; + $data->attributes = $attributes; + $data->source = $content; + return $data; + } +} diff --git a/src/Helper/HeadStyle.php b/src/Helper/HeadStyle.php new file mode 100644 index 00000000..61812342 --- /dev/null +++ b/src/Helper/HeadStyle.php @@ -0,0 +1,408 @@ +setSeparator(PHP_EOL); + } + + /** + * Return headStyle object + * + * Returns headStyle helper object; optionally, allows specifying + * + * @param string $content Stylesheet contents + * @param string $placement Append, prepend, or set + * @param string|array $attributes Optional attributes to utilize + * @return \Zend\View\Helper\HeadStyle + */ + public function __invoke($content = null, $placement = 'APPEND', $attributes = array()) + { + if ((null !== $content) && is_string($content)) { + switch (strtoupper($placement)) { + case 'SET': + $action = 'setStyle'; + break; + case 'PREPEND': + $action = 'prependStyle'; + break; + case 'APPEND': + default: + $action = 'appendStyle'; + break; + } + $this->$action($content, $attributes); + } + + return $this; + } + + /** + * Overload method calls + * + * Allows the following method calls: + * - appendStyle($content, $attributes = array()) + * - offsetSetStyle($index, $content, $attributes = array()) + * - prependStyle($content, $attributes = array()) + * - setStyle($content, $attributes = array()) + * + * @param string $method + * @param array $args + * @return void + * @throws Exception\BadMethodCallException When no $content provided or invalid method + */ + public function __call($method, $args) + { + if (preg_match('/^(?Pset|(ap|pre)pend|offsetSet)(Style)$/', $method, $matches)) { + $index = null; + $argc = count($args); + $action = $matches['action']; + + if ('offsetSet' == $action) { + if (0 < $argc) { + $index = array_shift($args); + --$argc; + } + } + + if (1 > $argc) { + throw new Exception\BadMethodCallException(sprintf( + 'Method "%s" requires minimally content for the stylesheet', + $method + )); + } + + $content = $args[0]; + $attrs = array(); + if (isset($args[1])) { + $attrs = (array) $args[1]; + } + + $item = $this->createData($content, $attrs); + + if ('offsetSet' == $action) { + $this->offsetSet($index, $item); + } else { + $this->$action($item); + } + + return $this; + } + + return parent::__call($method, $args); + } + + /** + * Determine if a value is a valid style tag + * + * @param mixed $value + * @param string $method + * @return boolean + */ + protected function isValid($value) + { + if ((!$value instanceof \stdClass) + || !isset($value->content) + || !isset($value->attributes)) + { + return false; + } + + return true; + } + + /** + * Override append to enforce style creation + * + * @param mixed $value + * @return void + * @throws Exception\InvalidArgumentException + */ + public function append($value) + { + if (!$this->isValid($value)) { + throw new Exception\InvalidArgumentException( + 'Invalid value passed to append; please use appendStyle()' + ); + } + + return $this->getContainer()->append($value); + } + + /** + * Override offsetSet to enforce style creation + * + * @param string|int $index + * @param mixed $value + * @return void + * @throws Exception\InvalidArgumentException + */ + public function offsetSet($index, $value) + { + if (!$this->isValid($value)) { + throw new Exception\InvalidArgumentException( + 'Invalid value passed to offsetSet; please use offsetSetStyle()' + ); + } + + return $this->getContainer()->offsetSet($index, $value); + } + + /** + * Override prepend to enforce style creation + * + * @param mixed $value + * @return void + * @throws Exception\InvalidArgumentException + */ + public function prepend($value) + { + if (!$this->isValid($value)) { + throw new Exception\InvalidArgumentException( + 'Invalid value passed to prepend; please use prependStyle()' + ); + } + + return $this->getContainer()->prepend($value); + } + + /** + * Override set to enforce style creation + * + * @param mixed $value + * @return void + * @throws Exception\InvalidArgumentException + */ + public function set($value) + { + if (!$this->isValid($value)) { + throw new Exception\InvalidArgumentException('Invalid value passed to set; please use setStyle()'); + } + + return $this->getContainer()->set($value); + } + + /** + * Start capture action + * + * @param mixed $captureType + * @param string $typeOrAttrs + * @return void + * @throws Exception\RuntimeException + */ + public function captureStart($type = Placeholder\Container\AbstractContainer::APPEND, $attrs = null) + { + if ($this->captureLock) { + throw new Exception\RuntimeException('Cannot nest headStyle captures'); + } + + $this->captureLock = true; + $this->captureAttrs = $attrs; + $this->captureType = $type; + ob_start(); + } + + /** + * End capture action and store + * + * @return void + */ + public function captureEnd() + { + $content = ob_get_clean(); + $attrs = $this->captureAttrs; + $this->captureAttrs = null; + $this->captureLock = false; + + switch ($this->captureType) { + case Placeholder\Container\AbstractContainer::SET: + $this->setStyle($content, $attrs); + break; + case Placeholder\Container\AbstractContainer::PREPEND: + $this->prependStyle($content, $attrs); + break; + case Placeholder\Container\AbstractContainer::APPEND: + default: + $this->appendStyle($content, $attrs); + break; + } + } + + /** + * Convert content and attributes into valid style tag + * + * @param stdClass $item Item to render + * @param string $indent Indentation to use + * @return string + */ + public function itemToString(\stdClass $item, $indent) + { + $attrString = ''; + if (!empty($item->attributes)) { + $enc = 'UTF-8'; + if ($this->view instanceof View\Renderer\RendererInterface + && method_exists($this->view, 'getEncoding') + ) { + $enc = $this->view->getEncoding(); + } + foreach ($item->attributes as $key => $value) { + if (!in_array($key, $this->optionalAttributes)) { + continue; + } + if ('media' == $key) { + if (false === strpos($value, ',')) { + if (!in_array($value, $this->mediaTypes)) { + continue; + } + } else { + $media_types = explode(',', $value); + $value = ''; + foreach ($media_types as $type) { + $type = trim($type); + if (!in_array($type, $this->mediaTypes)) { + continue; + } + $value .= $type .','; + } + $value = substr($value, 0, -1); + } + } + $attrString .= sprintf(' %s="%s"', $key, htmlspecialchars($value, ENT_COMPAT, $enc)); + } + } + + $escapeStart = $indent . ''. PHP_EOL; + if (isset($item->attributes['conditional']) + && !empty($item->attributes['conditional']) + && is_string($item->attributes['conditional']) + ) { + $escapeStart = null; + $escapeEnd = null; + } + + $html = ''; + + if (null == $escapeStart && null == $escapeEnd) { + $html = ''; + } + + return $html; + } + + /** + * Create string representation of placeholder + * + * @param string|int $indent + * @return string + */ + public function toString($indent = null) + { + $indent = (null !== $indent) + ? $this->getWhitespace($indent) + : $this->getIndent(); + + $items = array(); + $this->getContainer()->ksort(); + foreach ($this as $item) { + if (!$this->isValid($item)) { + continue; + } + $items[] = $this->itemToString($item, $indent); + } + + $return = $indent . implode($this->getSeparator() . $indent, $items); + $return = preg_replace("/(\r\n?|\n)/", '$1' . $indent, $return); + return $return; + } + + /** + * Create data item for use in stack + * + * @param string $content + * @param array $attributes + * @return stdClass + */ + public function createData($content, array $attributes) + { + if (!isset($attributes['media'])) { + $attributes['media'] = 'screen'; + } elseif (is_array($attributes['media'])) { + $attributes['media'] = implode(',', $attributes['media']); + } + + $data = new \stdClass(); + $data->content = $content; + $data->attributes = $attributes; + + return $data; + } +} diff --git a/src/Helper/HeadTitle.php b/src/Helper/HeadTitle.php new file mode 100644 index 00000000..5be3a1d0 --- /dev/null +++ b/src/Helper/HeadTitle.php @@ -0,0 +1,256 @@ +getDefaultAttachOrder()) + ? Placeholder\Container\AbstractContainer::APPEND + : $this->getDefaultAttachOrder(); + } + + $title = (string) $title; + if ($title !== '') { + if ($setType == Placeholder\Container\AbstractContainer::SET) { + $this->set($title); + } elseif ($setType == Placeholder\Container\AbstractContainer::PREPEND) { + $this->prepend($title); + } else { + $this->append($title); + } + } + + return $this; + } + + /** + * Set a default order to add titles + * + * @param string $setType + * @return void + * @throws Exception\DomainException + */ + public function setDefaultAttachOrder($setType) + { + if (!in_array($setType, array( + Placeholder\Container\AbstractContainer::APPEND, + Placeholder\Container\AbstractContainer::SET, + Placeholder\Container\AbstractContainer::PREPEND + ))) { + throw new Exception\DomainException( + "You must use a valid attach order: 'PREPEND', 'APPEND' or 'SET'" + ); + } + $this->defaultAttachOrder = $setType; + + return $this; + } + + /** + * Get the default attach order, if any. + * + * @return mixed + */ + public function getDefaultAttachOrder() + { + return $this->defaultAttachOrder; + } + + /** + * Turn helper into string + * + * @param string|null $indent + * @return string + */ + public function toString($indent = null) + { + $indent = (null !== $indent) + ? $this->getWhitespace($indent) + : $this->getIndent(); + + $items = array(); + + if (null !== ($translator = $this->getTranslator())) { + foreach ($this as $item) { + $items[] = $translator->translate( + $item, $this->getTranslatorTextDomain() + ); + } + } else { + foreach ($this as $item) { + $items[] = $item; + } + } + + $separator = $this->getSeparator(); + $output = ''; + + $prefix = $this->getPrefix(); + if ($prefix) { + $output .= $prefix; + } + + $output .= implode($separator, $items); + + $postfix = $this->getPostfix(); + if ($postfix) { + $output .= $postfix; + } + + $output = ($this->autoEscape) ? $this->escape($output) : $output; + + return $indent . '' . $output . ''; + } + + // Translator methods - Good candidate to refactor as a trait with PHP 5.4 + + /** + * Sets translator to use in helper + * + * @param Translator $translator [optional] translator. + * Default is null, which sets no translator. + * @param string $textDomain [optional] text domain + * Default is null, which skips setTranslatorTextDomain + * @return HeadTitle + */ + public function setTranslator(Translator $translator = null, $textDomain = null) + { + $this->translator = $translator; + if (null !== $textDomain) { + $this->setTranslatorTextDomain($textDomain); + } + return $this; + } + + /** + * Returns translator used in helper + * + * @return Translator|null + */ + public function getTranslator() + { + if (! $this->isTranslatorEnabled()) { + return null; + } + + return $this->translator; + } + + /** + * Checks if the helper has a translator + * + * @return bool + */ + public function hasTranslator() + { + return (bool) $this->getTranslator(); + } + + /** + * Sets whether translator is enabled and should be used + * + * @param bool $enabled [optional] whether translator should be used. + * Default is true. + * @return HeadTitle + */ + public function setTranslatorEnabled($enabled = true) + { + $this->translatorEnabled = (bool) $enabled; + return $this; + } + + /** + * Returns whether translator is enabled and should be used + * + * @return bool + */ + public function isTranslatorEnabled() + { + return $this->translatorEnabled; + } + + /** + * Set translation text domain + * + * @param string $textDomain + * @return HeadTitle + */ + public function setTranslatorTextDomain($textDomain = 'default') + { + $this->translatorTextDomain = $textDomain; + return $this; + } + + /** + * Return the translation text domain + * + * @return string + */ + public function getTranslatorTextDomain() + { + return $this->translatorTextDomain; + } +} diff --git a/src/Helper/HelperInterface.php b/src/Helper/HelperInterface.php new file mode 100644 index 00000000..1c3909be --- /dev/null +++ b/src/Helper/HelperInterface.php @@ -0,0 +1,37 @@ + $data, + 'quality' => 'high'), $params); + + $htmlObject = $this->getView()->plugin('htmlObject'); + return $htmlObject($data, self::TYPE, $attribs, $params, $content); + } +} diff --git a/src/Helper/HtmlList.php b/src/Helper/HtmlList.php new file mode 100644 index 00000000..3b0cba34 --- /dev/null +++ b/src/Helper/HtmlList.php @@ -0,0 +1,62 @@ +view->plugin('escapeHtml'); + $item = $escaper($item); + } + $list .= '
  • ' . $item . '
  • ' . self::EOL; + } else { + $itemLength = 5 + strlen(self::EOL); + if ($itemLength < strlen($list)) { + $list = substr($list, 0, strlen($list) - $itemLength) + . $this($item, $ordered, $attribs, $escape) . '' . self::EOL; + } else { + $list .= '
  • ' . $this($item, $ordered, $attribs, $escape) . '
  • ' . self::EOL; + } + } + } + + if ($attribs) { + $attribs = $this->htmlAttribs($attribs); + } else { + $attribs = ''; + } + + $tag = ($ordered) ? 'ol' : 'ul'; + + return '<' . $tag . $attribs . '>' . self::EOL . $list . '' . self::EOL; + } +} diff --git a/src/Helper/HtmlObject.php b/src/Helper/HtmlObject.php new file mode 100644 index 00000000..b272e613 --- /dev/null +++ b/src/Helper/HtmlObject.php @@ -0,0 +1,70 @@ + $data, + 'type' => $type), $attribs); + + // Params + $paramHtml = array(); + $closingBracket = $this->getClosingBracket(); + + foreach ($params as $param => $options) { + if (is_string($options)) { + $options = array('value' => $options); + } + + $options = array_merge(array('name' => $param), $options); + + $paramHtml[] = 'htmlAttribs($options) . $closingBracket; + } + + // Content + if (is_array($content)) { + $content = implode(self::EOL, $content); + } + + // Object header + $xhtml = 'htmlAttribs($attribs) . '>' . self::EOL + . implode(self::EOL, $paramHtml) . self::EOL + . ($content ? $content . self::EOL : '') + . ''; + + return $xhtml; + } +} diff --git a/src/Helper/HtmlPage.php b/src/Helper/HtmlPage.php new file mode 100644 index 00000000..b7fc9c30 --- /dev/null +++ b/src/Helper/HtmlPage.php @@ -0,0 +1,59 @@ + self::ATTRIB_CLASSID); + + /** + * Output a html object tag + * + * @param string $data The html url + * @param array $attribs Attribs for the object tag + * @param array $params Params for in the object tag + * @param string $content Alternative content + * @return string + */ + public function __invoke($data, array $attribs = array(), array $params = array(), $content = null) + { + // Attrs + $attribs = array_merge($this->attribs, $attribs); + + // Params + $params = array_merge(array('data' => $data), $params); + + $htmlObject = $this->getView()->plugin('htmlObject'); + return $htmlObject($data, self::TYPE, $attribs, $params, $content); + } +} diff --git a/src/Helper/HtmlQuicktime.php b/src/Helper/HtmlQuicktime.php new file mode 100644 index 00000000..3983d761 --- /dev/null +++ b/src/Helper/HtmlQuicktime.php @@ -0,0 +1,66 @@ + self::ATTRIB_CLASSID, + 'codebase' => self::ATTRIB_CODEBASE); + + /** + * Output a quicktime movie object tag + * + * @param string $data The quicktime file + * @param array $attribs Attribs for the object tag + * @param array $params Params for in the object tag + * @param string $content Alternative content + * @return string + */ + public function __invoke($data, array $attribs = array(), array $params = array(), $content = null) + { + // Attrs + $attribs = array_merge($this->attribs, $attribs); + + // Params + $params = array_merge(array('src' => $data), $params); + + $htmlObject = $this->getView()->plugin('htmlObject'); + return $htmlObject($data, self::TYPE, $attribs, $params, $content); + } +} diff --git a/src/Helper/InlineScript.php b/src/Helper/InlineScript.php new file mode 100644 index 00000000..47ab9e4d --- /dev/null +++ b/src/Helper/InlineScript.php @@ -0,0 +1,45 @@ +response = $response; + return $this; + } + + /** + * Encode data as JSON and set response header + * + * @param mixed $data + * @param array $jsonOptions Options to pass to JsonFormatter::encode() + * @return string|void + */ + public function __invoke($data, array $jsonOptions = array()) + { + $data = JsonFormatter::encode($data, null, $jsonOptions); + + if ($this->response instanceof Response) { + $headers = $this->response->getHeaders(); + $headers->addHeaderLine('Content-Type', 'application/json'); + } + + return $data; + } +} diff --git a/src/Helper/Layout.php b/src/Helper/Layout.php new file mode 100644 index 00000000..e9787e41 --- /dev/null +++ b/src/Helper/Layout.php @@ -0,0 +1,101 @@ +getRoot(); + return $model->getTemplate(); + } + + /** + * Set layout template + * + * @param string $template + * @return Layout + */ + public function setTemplate($template) + { + $model = $this->getRoot(); + $model->setTemplate((string) $template); + return $this; + } + + /** + * Set layout template or retrieve "layout" view model + * + * If no arguments are given, grabs the "root" or "layout" view model. + * Otherwise, attempts to set the template for that view model. + * + * @param null|string $template + * @return Layout + */ + public function __invoke($template = null) + { + if (null === $template) { + return $this->getRoot(); + } + return $this->setTemplate($template); + } + + /** + * Get the root view model + * + * @return null|Model + */ + protected function getRoot() + { + $helper = $this->getViewModelHelper(); + if (!$helper->hasRoot()) { + throw new Exception\RuntimeException(sprintf( + '%s: no view model currently registered as root in renderer', + __METHOD__ + )); + } + return $helper->getRoot(); + } + + /** + * Retrieve the view model helper + * + * @return ViewModel + */ + protected function getViewModelHelper() + { + if ($this->viewModelHelper) { + return $this->viewModelHelper; + } + $view = $this->getView(); + $this->viewModelHelper = $view->plugin('view_model'); + return $this->viewModelHelper; + } +} diff --git a/src/Helper/Navigation.php b/src/Helper/Navigation.php new file mode 100644 index 00000000..a36f24a6 --- /dev/null +++ b/src/Helper/Navigation.php @@ -0,0 +1,343 @@ +setContainer($container); + } + + return $this; + } + + /** + * Magic overload: Proxy to other navigation helpers or the container + * + * Examples of usage from a view script or layout: + * + * // proxy to Menu helper and render container: + * echo $this->navigation()->menu(); + * + * // proxy to Breadcrumbs helper and set indentation: + * $this->navigation()->breadcrumbs()->setIndent(8); + * + * // proxy to container and find all pages with 'blog' route: + * $blogPages = $this->navigation()->findAllByRoute('blog'); + * + * + * @param string $method helper name or method name in + * container + * @param array $arguments [optional] arguments to pass + * @return mixed returns what the proxied call returns + * @throws \Zend\View\Exception\ExceptionInterface if proxying to a helper, and the + * helper is not an instance of the + * interface specified in + * {@link findHelper()} + * @throws \Zend\Navigation\Exception\ExceptionInterface if method does not exist in container + */ + public function __call($method, array $arguments = array()) + { + // check if call should proxy to another helper + $helper = $this->findHelper($method, false); + if ($helper) { + if ($helper instanceof ServiceLocatorAwareInterface && $this->getServiceLocator()) { + $helper->setServiceLocator($this->getServiceLocator()); + } + return call_user_func_array($helper, $arguments); + } + + // default behaviour: proxy call to container + return parent::__call($method, $arguments); + } + + /** + * Set manager for retrieving navigation helpers + * + * @param Navigation\PluginManager $plugins + * @return Navigation + */ + public function setPluginManager(Navigation\PluginManager $plugins) + { + $renderer = $this->getView(); + if ($renderer) { + $plugins->setRenderer($renderer); + } + $this->plugins = $plugins; + return $this; + } + + /** + * Retrieve plugin loader for navigation helpers + * + * Lazy-loads an instance of Navigation\HelperLoader if none currently + * registered. + * + * @return Navigation\PluginManager + */ + public function getPluginManager() + { + if (null === $this->plugins) { + $this->setPluginManager(new Navigation\PluginManager()); + } + return $this->plugins; + } + + /** + * Returns the helper matching $proxy + * + * The helper must implement the interface + * {@link Zend\View\Helper\Navigation\Helper}. + * + * @param string $proxy helper name + * @param bool $strict [optional] whether + * exceptions should be + * thrown if something goes + * wrong. Default is true. + * @return \Zend\View\Helper\Navigation\HelperInterface helper instance + * @throws Exception\RuntimeException if $strict is true and + * helper cannot be found + */ + public function findHelper($proxy, $strict = true) + { + $plugins = $this->getPluginManager(); + if (!$plugins->has($proxy)) { + if ($strict) { + throw new Exception\RuntimeException(sprintf( + 'Failed to find plugin for %s', + $proxy + )); + } + return false; + } + + $helper = $plugins->get($proxy); + $class = get_class($helper); + + if (!isset($this->injected[$class])) { + $this->inject($helper); + $this->injected[$class] = true; + } + + return $helper; + } + + /** + * Injects container, ACL, and translator to the given $helper if this + * helper is configured to do so + * + * @param NavigationHelper $helper helper instance + * @return void + */ + protected function inject(NavigationHelper $helper) + { + if ($this->getInjectContainer() && !$helper->hasContainer()) { + $helper->setContainer($this->getContainer()); + } + + if ($this->getInjectAcl()) { + if (!$helper->hasAcl()) { + $helper->setAcl($this->getAcl()); + } + if (!$helper->hasRole()) { + $helper->setRole($this->getRole()); + } + } + + if ($this->getInjectTranslator() && !$helper->hasTranslator()) { + $helper->setTranslator( + $this->getTranslator(), $this->getTranslatorTextDomain() + ); + } + } + + // Accessors: + + /** + * Sets the default proxy to use in {@link render()} + * + * @param string $proxy default proxy + * @return \Zend\View\Helper\Navigation fluent interface, returns self + */ + public function setDefaultProxy($proxy) + { + $this->defaultProxy = (string) $proxy; + return $this; + } + + /** + * Returns the default proxy to use in {@link render()} + * + * @return string the default proxy to use in {@link render()} + */ + public function getDefaultProxy() + { + return $this->defaultProxy; + } + + /** + * Sets whether container should be injected when proxying + * + * @param bool $injectContainer [optional] whether container should + * be injected when proxying. Default + * is true. + * @return \Zend\View\Helper\Navigation fluent interface, returns self + */ + public function setInjectContainer($injectContainer = true) + { + $this->injectContainer = (bool) $injectContainer; + return $this; + } + + /** + * Returns whether container should be injected when proxying + * + * @return bool whether container should be injected when proxying + */ + public function getInjectContainer() + { + return $this->injectContainer; + } + + /** + * Sets whether ACL should be injected when proxying + * + * @param bool $injectAcl [optional] whether ACL should be + * injected when proxying. Default is + * true. + * @return \Zend\View\Helper\Navigation fluent interface, returns self + */ + public function setInjectAcl($injectAcl = true) + { + $this->injectAcl = (bool) $injectAcl; + return $this; + } + + /** + * Returns whether ACL should be injected when proxying + * + * @return bool whether ACL should be injected when proxying + */ + public function getInjectAcl() + { + return $this->injectAcl; + } + + /** + * Sets whether translator should be injected when proxying + * + * @param bool $injectTranslator [optional] whether translator should + * be injected when proxying. Default + * is true. + * @return Navigation fluent interface, returns self + */ + public function setInjectTranslator($injectTranslator = true) + { + $this->injectTranslator = (bool) $injectTranslator; + return $this; + } + + /** + * Returns whether translator should be injected when proxying + * + * @return bool whether translator should be injected when proxying + */ + public function getInjectTranslator() + { + return $this->injectTranslator; + } + + // Zend\View\Helper\Navigation\Helper: + + /** + * Renders helper + * + * @param \Zend\Navigation\AbstractContainer $container [optional] container to + * render. Default is to + * render the container + * registered in the helper. + * @return string helper output + * @throws Exception\RuntimeException if helper cannot be found + */ + public function render($container = null) + { + $helper = $this->findHelper($this->getDefaultProxy()); + return $helper->render($container); + } +} diff --git a/src/Helper/Navigation/AbstractHelper.php b/src/Helper/Navigation/AbstractHelper.php new file mode 100644 index 00000000..ce87945f --- /dev/null +++ b/src/Helper/Navigation/AbstractHelper.php @@ -0,0 +1,880 @@ +serviceLocator = $serviceLocator; + return $this; + } + + /** + * Get the service locator. + * + * @return \Zend\ServiceManager\ServiceLocatorInterface + */ + public function getServiceLocator() + { + return $this->serviceLocator; + } + + /** + * Sets navigation container the helper operates on by default + * + * Implements {@link HelperInterface::setContainer()}. + * + * @param string|Navigation\AbstractContainer $container [optional] container to operate on. + * Default is null, meaning container will be reset. + * @return AbstractHelper fluent interface, returns self + */ + public function setContainer($container = null) + { + $this->parseContainer($container); + $this->container = $container; + return $this; + } + + /** + * Returns the navigation container helper operates on by default + * + * Implements {@link HelperInterface::getContainer()}. + * + * If no container is set, a new container will be instantiated and + * stored in the helper. + * + * @return Navigation\AbstractContainer navigation container + */ + public function getContainer() + { + if (null === $this->container) { + $this->container = new \Zend\Navigation\Navigation(); + } + + return $this->container; + } + + /** + * Verifies container and eventually fetches it from service locator if it is a string + * + * @param \Zend\Navigation\AbstractContainer|string|null $container + * @throws \Zend\View\Exception\InvalidArgumentException + */ + protected function parseContainer(&$container = null) + { + if (null === $container) { + return; + } + + if (is_string($container)) { + if (!$this->getServiceLocator()) { + throw new Exception\InvalidArgumentException(sprintf( + 'Attempted to set container with alias "%s" but no ServiceLocator was set', + $container + )); + } + + /** + * Load the navigation container from the root service locator + * + * The navigation container is probably located in Zend\ServiceManager\ServiceManager + * and not in the Zend\View\HelperPluginManager. If the set service locator is a + * HelperPluginManager, access the navigation container via the main service locator. + */ + $sl = $this->getServiceLocator(); + if ($sl instanceof View\HelperPluginManager) { + $sl = $sl->getServiceLocator(); + } + $container = $sl->get($container); + return; + } + + if (!$container instanceof Navigation\AbstractContainer) { + throw new Exception\InvalidArgumentException( + 'Container must be a string alias or an instance of ' . + 'Zend\Navigation\AbstractContainer' + ); + } + } + + /** + * Sets the minimum depth a page must have to be included when rendering + * + * @param int $minDepth [optional] minimum depth. Default is null, which + * sets no minimum depth. + * @return AbstractHelper fluent interface, returns self + */ + public function setMinDepth($minDepth = null) + { + if (null === $minDepth || is_int($minDepth)) { + $this->minDepth = $minDepth; + } else { + $this->minDepth = (int) $minDepth; + } + return $this; + } + + /** + * Returns minimum depth a page must have to be included when rendering + * + * @return int|null minimum depth or null + */ + public function getMinDepth() + { + if (!is_int($this->minDepth) || $this->minDepth < 0) { + return 0; + } + return $this->minDepth; + } + + /** + * Sets the maximum depth a page can have to be included when rendering + * + * @param int $maxDepth [optional] maximum depth. Default is null, which + * sets no maximum depth. + * @return AbstractHelper fluent interface, returns self + */ + public function setMaxDepth($maxDepth = null) + { + if (null === $maxDepth || is_int($maxDepth)) { + $this->maxDepth = $maxDepth; + } else { + $this->maxDepth = (int) $maxDepth; + } + return $this; + } + + /** + * Returns maximum depth a page can have to be included when rendering + * + * @return int|null maximum depth or null + */ + public function getMaxDepth() + { + return $this->maxDepth; + } + + /** + * Set the indentation string for using in {@link render()}, optionally a + * number of spaces to indent with + * + * @param string|int $indent indentation string or number of spaces + * @return AbstractHelper fluent interface, returns self + */ + public function setIndent($indent) + { + $this->indent = $this->getWhitespace($indent); + return $this; + } + + /** + * Returns indentation + * + * @return string + */ + public function getIndent() + { + return $this->indent; + } + + /** + * Sets ACL to use when iterating pages + * + * Implements {@link HelperInterface::setAcl()}. + * + * @param Acl\Acl $acl [optional] ACL object. Default is null. + * @return AbstractHelper fluent interface, returns self + */ + public function setAcl(Acl\Acl $acl = null) + { + $this->acl = $acl; + return $this; + } + + /** + * Returns ACL or null if it isn't set using {@link setAcl()} or + * {@link setDefaultAcl()} + * + * Implements {@link HelperInterface::getAcl()}. + * + * @return Acl\Acl|null ACL object or null + */ + public function getAcl() + { + if ($this->acl === null && self::$defaultAcl !== null) { + return self::$defaultAcl; + } + + return $this->acl; + } + + /** + * Sets ACL role(s) to use when iterating pages + * + * Implements {@link HelperInterface::setRole()}. + * + * @param mixed $role [optional] role to set. Expects a string, an + * instance of type {@link Acl\Role\RoleInterface}, or null. Default + * is null, which will set no role. + * @return AbstractHelper fluent interface, returns self + * @throws Exception\InvalidArgumentException if $role is invalid + */ + public function setRole($role = null) + { + if (null === $role || is_string($role) || + $role instanceof Acl\Role\RoleInterface + ) { + $this->role = $role; + } else { + throw new Exception\InvalidArgumentException(sprintf( + '$role must be a string, null, or an instance of ' + . 'Zend\Permissions\Role\RoleInterface; %s given', + (is_object($role) ? get_class($role) : gettype($role)) + )); + } + + return $this; + } + + /** + * Returns ACL role to use when iterating pages, or null if it isn't set + * using {@link setRole()} or {@link setDefaultRole()} + * + * Implements {@link HelperInterface::getRole()}. + * + * @return string|Acl\Role\RoleInterface|null role or null + */ + public function getRole() + { + if ($this->role === null && self::$defaultRole !== null) { + return self::$defaultRole; + } + + return $this->role; + } + + /** + * Sets whether ACL should be used + * + * Implements {@link HelperInterface::setUseAcl()}. + * + * @param bool $useAcl [optional] whether ACL should be used. Default is true. + * @return AbstractHelper fluent interface, returns self + */ + public function setUseAcl($useAcl = true) + { + $this->useAcl = (bool) $useAcl; + return $this; + } + + /** + * Returns whether ACL should be used + * + * Implements {@link HelperInterface::getUseAcl()}. + * + * @return bool whether ACL should be used + */ + public function getUseAcl() + { + return $this->useAcl; + } + + /** + * Return renderInvisible flag + * + * @return bool + */ + public function getRenderInvisible() + { + return $this->renderInvisible; + } + + /** + * Render invisible items? + * + * @param bool $renderInvisible [optional] boolean flag + * @return AbstractHelper fluent interface returns self + */ + public function setRenderInvisible($renderInvisible = true) + { + $this->renderInvisible = (bool) $renderInvisible; + return $this; + } + + // Magic overloads: + + /** + * Magic overload: Proxy calls to the navigation container + * + * @param string $method method name in container + * @param array $arguments [optional] arguments to pass + * @return mixed returns what the container returns + * @throws Navigation\Exception\ExceptionInterface if method does not exist in container + */ + public function __call($method, array $arguments = array()) + { + return call_user_func_array( + array($this->getContainer(), $method), + $arguments); + } + + /** + * Magic overload: Proxy to {@link render()}. + * + * This method will trigger an E_USER_ERROR if rendering the helper causes + * an exception to be thrown. + * + * Implements {@link HelperInterface::__toString()}. + * + * @return string + */ + public function __toString() + { + try { + return $this->render(); + } catch (\Exception $e) { + $msg = get_class($e) . ': ' . $e->getMessage(); + trigger_error($msg, E_USER_ERROR); + return ''; + } + } + + // Public methods: + + /** + * Finds the deepest active page in the given container + * + * @param Navigation\AbstractContainer $container container to search + * @param int|null $minDepth [optional] minimum depth + * required for page to be + * valid. Default is to use + * {@link getMinDepth()}. A + * null value means no minimum + * depth required. + * @param int|null $minDepth [optional] maximum depth + * a page can have to be + * valid. Default is to use + * {@link getMaxDepth()}. A + * null value means no maximum + * depth required. + * @return array an associative array with + * the values 'depth' and + * 'page', or an empty array + * if not found + */ + public function findActive($container, $minDepth = null, $maxDepth = -1) + { + $this->parseContainer($container); + if (!is_int($minDepth)) { + $minDepth = $this->getMinDepth(); + } + if ((!is_int($maxDepth) || $maxDepth < 0) && null !== $maxDepth) { + $maxDepth = $this->getMaxDepth(); + } + + $found = null; + $foundDepth = -1; + $iterator = new RecursiveIteratorIterator($container, RecursiveIteratorIterator::CHILD_FIRST); + + foreach ($iterator as $page) { + $currDepth = $iterator->getDepth(); + if ($currDepth < $minDepth || !$this->accept($page)) { + // page is not accepted + continue; + } + + if ($page->isActive(false) && $currDepth > $foundDepth) { + // found an active page at a deeper level than before + $found = $page; + $foundDepth = $currDepth; + } + } + + if (is_int($maxDepth) && $foundDepth > $maxDepth) { + while ($foundDepth > $maxDepth) { + if (--$foundDepth < $minDepth) { + $found = null; + break; + } + + $found = $found->getParent(); + if (!$found instanceof AbstractPage) { + $found = null; + break; + } + } + } + + if ($found) { + return array('page' => $found, 'depth' => $foundDepth); + } else { + return array(); + } + } + + /** + * Checks if the helper has a container + * + * Implements {@link HelperInterface::hasContainer()}. + * + * @return bool whether the helper has a container or not + */ + public function hasContainer() + { + return null !== $this->container; + } + + /** + * Checks if the helper has an ACL instance + * + * Implements {@link HelperInterface::hasAcl()}. + * + * @return bool whether the helper has a an ACL instance or not + */ + public function hasAcl() + { + return null !== $this->acl; + } + + /** + * Checks if the helper has an ACL role + * + * Implements {@link HelperInterface::hasRole()}. + * + * @return bool whether the helper has a an ACL role or not + */ + public function hasRole() + { + return null !== $this->role; + } + + /** + * Returns an HTML string containing an 'a' element for the given page + * + * @param AbstractPage $page page to generate HTML for + * @return string HTML string for the given page + */ + public function htmlify(AbstractPage $page) + { + // get label and title for translating + $label = $page->getLabel(); + $title = $page->getTitle(); + + if (null !== ($translator = $this->getTranslator())) { + $textDomain = $this->getTranslatorTextDomain(); + if (is_string($label) && !empty($label)) { + $label = $translator->translate($label, $textDomain); + } + if (is_string($title) && !empty($title)) { + $title = $translator->translate($title, $textDomain); + } + } + + // get attribs for anchor element + $attribs = array( + 'id' => $page->getId(), + 'title' => $title, + 'class' => $page->getClass(), + 'href' => $page->getHref(), + 'target' => $page->getTarget() + ); + + $escaper = $this->view->plugin('escapeHtml'); + + return 'htmlAttribs($attribs) . '>' + . $escaper($label) + . ''; + } + + // Translator methods - Good candidate to refactor as a trait with PHP 5.4 + + /** + * Sets translator to use in helper + * + * @param Translator $translator [optional] translator. + * Default is null, which sets no translator. + * @param string $textDomain [optional] text domain + * Default is null, which skips setTranslatorTextDomain + * @return AbstractHelper + */ + public function setTranslator(Translator $translator = null, $textDomain = null) + { + $this->translator = $translator; + if (null !== $textDomain) { + $this->setTranslatorTextDomain($textDomain); + } + return $this; + } + + /** + * Returns translator used in helper + * + * @return Translator|null + */ + public function getTranslator() + { + if (! $this->isTranslatorEnabled()) { + return null; + } + + return $this->translator; + } + + /** + * Checks if the helper has a translator + * + * @return bool + */ + public function hasTranslator() + { + return (bool) $this->getTranslator(); + } + + /** + * Sets whether translator is enabled and should be used + * + * @param bool $enabled [optional] whether translator should be used. + * Default is true. + * @return AbstractHelper + */ + public function setTranslatorEnabled($enabled = true) + { + $this->translatorEnabled = (bool) $enabled; + return $this; + } + + /** + * Returns whether translator is enabled and should be used + * + * @return bool + */ + public function isTranslatorEnabled() + { + return $this->translatorEnabled; + } + + /** + * Set translation text domain + * + * @param string $textDomain + * @return AbstractHelper + */ + public function setTranslatorTextDomain($textDomain = 'default') + { + $this->translatorTextDomain = $textDomain; + return $this; + } + + /** + * Return the translation text domain + * + * @return string + */ + public function getTranslatorTextDomain() + { + return $this->translatorTextDomain; + } + + // Iterator filter methods: + + /** + * Determines whether a page should be accepted when iterating + * + * Rules: + * - If a page is not visible it is not accepted, unless RenderInvisible has + * been set to true. + * - If helper has no ACL, page is accepted + * - If helper has ACL, but no role, page is not accepted + * - If helper has ACL and role: + * - Page is accepted if it has no resource or privilege + * - Page is accepted if ACL allows page's resource or privilege + * - If page is accepted by the rules above and $recursive is true, the page + * will not be accepted if it is the descendant of a non-accepted page. + * + * @param AbstractPage $page page to check + * @param bool $recursive [optional] if true, page will not be + * accepted if it is the descendant of a + * page that is not accepted. Default is true. + * @return bool whether page should be accepted + */ + public function accept(AbstractPage $page, $recursive = true) + { + // accept by default + $accept = true; + + if (!$page->isVisible(false) && !$this->getRenderInvisible()) { + // don't accept invisible pages + $accept = false; + } elseif ($this->getUseAcl() && !$this->acceptAcl($page)) { + // acl is not amused + $accept = false; + } + + if ($accept && $recursive) { + $parent = $page->getParent(); + if ($parent instanceof AbstractPage) { + $accept = $this->accept($parent, true); + } + } + + return $accept; + } + + /** + * Determines whether a page should be accepted by ACL when iterating + * + * Rules: + * - If helper has no ACL, page is accepted + * - If page has a resource or privilege defined, page is accepted + * if the ACL allows access to it using the helper's role + * - If page has no resource or privilege, page is accepted + * + * @param AbstractPage $page page to check + * @return bool whether page is accepted by ACL + */ + protected function acceptAcl(AbstractPage $page) + { + if (!$acl = $this->getAcl()) { + // no acl registered means don't use acl + return true; + } + + $role = $this->getRole(); + $resource = $page->getResource(); + $privilege = $page->getPrivilege(); + + if ($resource || $privilege) { + // determine using helper role and page resource/privilege + return $acl->hasResource($resource) && $acl->isAllowed($role, $resource, $privilege); + } + + return true; + } + + // Util methods: + + /** + * Retrieve whitespace representation of $indent + * + * @param int|string $indent + * @return string + */ + protected function getWhitespace($indent) + { + if (is_int($indent)) { + $indent = str_repeat(' ', $indent); + } + + return (string) $indent; + } + + /** + * Converts an associative array to a string of tag attributes. + * + * Overloads {@link View\Helper\AbstractHtmlElement::htmlAttribs()}. + * + * @param array $attribs an array where each key-value pair is converted + * to an attribute name and value + * @return string an attribute string + */ + protected function htmlAttribs($attribs) + { + // filter out null values and empty string values + foreach ($attribs as $key => $value) { + if ($value === null || (is_string($value) && !strlen($value))) { + unset($attribs[$key]); + } + } + + return parent::htmlAttribs($attribs); + } + + /** + * Normalize an ID + * + * Overrides {@link View\Helper\AbstractHtmlElement::normalizeId()}. + * + * @param string $value + * @return string + */ + protected function normalizeId($value) + { + $prefix = get_class($this); + $prefix = strtolower(trim(substr($prefix, strrpos($prefix, '\\')), '\\')); + + return $prefix . '-' . $value; + } + + // Static methods: + + /** + * Sets default ACL to use if another ACL is not explicitly set + * + * @param Acl\Acl $acl [optional] ACL object. Default is null, which + * sets no ACL object. + * @return void + */ + public static function setDefaultAcl(Acl\Acl $acl = null) + { + self::$defaultAcl = $acl; + } + + /** + * Sets default ACL role(s) to use when iterating pages if not explicitly + * set later with {@link setRole()} + * + * @param mixed $role [optional] role to set. Expects null, string, or an + * instance of {@link Acl\Role\RoleInterface}. Default is null, which + * sets no default role. + * @return void + * @throws Exception\InvalidArgumentException if role is invalid + */ + public static function setDefaultRole($role = null) + { + if (null === $role + || is_string($role) + || $role instanceof Acl\Role\RoleInterface + ) { + self::$defaultRole = $role; + } else { + throw new Exception\InvalidArgumentException(sprintf( + '$role must be null|string|Zend\Permissions\Role\RoleInterface; received "%s"', + (is_object($role) ? get_class($role) : gettype($role)) + )); + } + } +} diff --git a/src/Helper/Navigation/Breadcrumbs.php b/src/Helper/Navigation/Breadcrumbs.php new file mode 100644 index 00000000..3e05a713 --- /dev/null +++ b/src/Helper/Navigation/Breadcrumbs.php @@ -0,0 +1,298 @@ +setContainer($container); + } + + return $this; + } + + /** + * Sets breadcrumb separator + * + * @param string $separator separator string + * @return Breadcrumbs fluent interface, returns self + */ + public function setSeparator($separator) + { + if (is_string($separator)) { + $this->separator = $separator; + } + + return $this; + } + + /** + * Returns breadcrumb separator + * + * @return string breadcrumb separator + */ + public function getSeparator() + { + return $this->separator; + } + + /** + * Sets whether last page in breadcrumbs should be hyperlinked + * + * @param bool $linkLast whether last page should be hyperlinked + * @return Breadcrumbs fluent interface, returns self + */ + public function setLinkLast($linkLast) + { + $this->linkLast = (bool) $linkLast; + return $this; + } + + /** + * Returns whether last page in breadcrumbs should be hyperlinked + * + * @return bool whether last page in breadcrumbs should be hyperlinked + */ + public function getLinkLast() + { + return $this->linkLast; + } + + /** + * Sets which partial view script to use for rendering menu + * + * @param string|array $partial partial view script or null. If an array is + * given, it is expected to contain two + * values; the partial view script to use, + * and the module where the script can be + * found. + * @return Breadcrumbs fluent interface, returns self + */ + public function setPartial($partial) + { + if (null === $partial || is_string($partial) || is_array($partial)) { + $this->partial = $partial; + } + + return $this; + } + + /** + * Returns partial view script to use for rendering menu + * + * @return string|array|null + */ + public function getPartial() + { + return $this->partial; + } + + // Render methods: + + /** + * Renders breadcrumbs by chaining 'a' elements with the separator + * registered in the helper + * + * @param AbstractContainer $container [optional] container to render. Default is + * to render the container registered in the helper. + * @return string helper output + */ + public function renderStraight($container = null) + { + $this->parseContainer($container); + if (null === $container) { + $container = $this->getContainer(); + } + + // find deepest active + if (!$active = $this->findActive($container)) { + return ''; + } + + $active = $active['page']; + + // put the deepest active page last in breadcrumbs + if ($this->getLinkLast()) { + $html = $this->htmlify($active); + } else { + $html = $active->getLabel(); + if (null !== ($translator = $this->getTranslator())) { + $html = $translator->translate($html, $this->getTranslatorTextDomain()); + } + $escaper = $this->view->plugin('escapeHtml'); + $html = $escaper($html); + } + + // walk back to root + while ($parent = $active->getParent()) { + if ($parent instanceof AbstractPage) { + // prepend crumb to html + $html = $this->htmlify($parent) + . $this->getSeparator() + . $html; + } + + if ($parent === $container) { + // at the root of the given container + break; + } + + $active = $parent; + } + + return strlen($html) ? $this->getIndent() . $html : ''; + } + + /** + * Renders the given $container by invoking the partial view helper + * + * The container will simply be passed on as a model to the view script, + * so in the script it will be available in $this->container. + * + * @param AbstractContainer $container [optional] container to pass to view script. + * Default is to use the container registered + * in the helper. + * @param string|array $partial [optional] partial view script to use. + * Default is to use the partial registered + * in the helper. If an array is given, it + * is expected to contain two values; the + * partial view script to use, and the module + * where the script can be found. + * @return string helper output + * @throws Exception\RuntimeException if no partial provided + * @throws Exception\InvalidArgumentException if partial is invalid array + */ + public function renderPartial($container = null, $partial = null) + { + $this->parseContainer($container); + if (null === $container) { + $container = $this->getContainer(); + } + + if (null === $partial) { + $partial = $this->getPartial(); + } + + if (empty($partial)) { + throw new Exception\RuntimeException( + 'Unable to render menu: No partial view script provided' + ); + } + + // put breadcrumb pages in model + $model = array('pages' => array()); + $active = $this->findActive($container); + if ($active) { + $active = $active['page']; + $model['pages'][] = $active; + while ($parent = $active->getParent()) { + if ($parent instanceof AbstractPage) { + $model['pages'][] = $parent; + } else { + break; + } + + if ($parent === $container) { + // break if at the root of the given container + break; + } + + $active = $parent; + } + $model['pages'] = array_reverse($model['pages']); + } + + if (is_array($partial)) { + if (count($partial) != 2) { + throw new Exception\InvalidArgumentException( + 'Unable to render menu: A view partial supplied as ' + . 'an array must contain two values: partial view ' + . 'script and module where script can be found' + ); + } + + $partialHelper = $this->view->plugin('partial'); + return $partialHelper($partial[0], /*$partial[1], */$model); + } + + $partialHelper = $this->view->plugin('partial'); + return $partialHelper($partial, $model); + } + + // Zend\View\Helper\Navigation\Helper: + + /** + * Renders helper + * + * Implements {@link HelperInterface::render()}. + * + * @param AbstractContainer $container [optional] container to render. Default is + * to render the container registered in the helper. + * @return string helper output + */ + public function render($container = null) + { + $partial = $this->getPartial(); + if ($partial) { + return $this->renderPartial($container, $partial); + } else { + return $this->renderStraight($container); + } + } +} diff --git a/src/Helper/Navigation/HelperInterface.php b/src/Helper/Navigation/HelperInterface.php new file mode 100644 index 00000000..eb64181b --- /dev/null +++ b/src/Helper/Navigation/HelperInterface.php @@ -0,0 +1,150 @@ + elements + * + * @category Zend + * @package Zend_View + * @subpackage Helper + */ +class Links extends AbstractHelper +{ + /**#@+ + * Constants used for specifying which link types to find and render + * + * @var int + */ + const RENDER_ALTERNATE = 0x0001; + const RENDER_STYLESHEET = 0x0002; + const RENDER_START = 0x0004; + const RENDER_NEXT = 0x0008; + const RENDER_PREV = 0x0010; + const RENDER_CONTENTS = 0x0020; + const RENDER_INDEX = 0x0040; + const RENDER_GLOSSARY = 0x0080; + const RENDER_COPYRIGHT = 0x0100; + const RENDER_CHAPTER = 0x0200; + const RENDER_SECTION = 0x0400; + const RENDER_SUBSECTION = 0x0800; + const RENDER_APPENDIX = 0x1000; + const RENDER_HELP = 0x2000; + const RENDER_BOOKMARK = 0x4000; + const RENDER_CUSTOM = 0x8000; + const RENDER_ALL = 0xffff; + /**#@+**/ + + /** + * Maps render constants to W3C link types + * + * @var array + */ + protected static $RELATIONS = array( + self::RENDER_ALTERNATE => 'alternate', + self::RENDER_STYLESHEET => 'stylesheet', + self::RENDER_START => 'start', + self::RENDER_NEXT => 'next', + self::RENDER_PREV => 'prev', + self::RENDER_CONTENTS => 'contents', + self::RENDER_INDEX => 'index', + self::RENDER_GLOSSARY => 'glossary', + self::RENDER_COPYRIGHT => 'copyright', + self::RENDER_CHAPTER => 'chapter', + self::RENDER_SECTION => 'section', + self::RENDER_SUBSECTION => 'subsection', + self::RENDER_APPENDIX => 'appendix', + self::RENDER_HELP => 'help', + self::RENDER_BOOKMARK => 'bookmark', + ); + + /** + * The helper's render flag + * + * @see render() + * @see setRenderFlag() + * @var int + */ + protected $renderFlag = self::RENDER_ALL; + + /** + * Root container + * + * Used for preventing methods to traverse above the container given to + * the {@link render()} method. + * + * @see _findRoot() + * + * @var AbstractContainer + */ + protected $root; + + /** + * Helper entry point + * + * @param string|AbstractContainer $container container to operate on + * @return Links + */ + public function __invoke($container = null) + { + if (null !== $container) { + $this->setContainer($container); + } + + return $this; + } + + /** + * Magic overload: Proxy calls to {@link findRelation()} or container + * + * Examples of finder calls: + * + * // METHOD // SAME AS + * $h->findRelNext($page); // $h->findRelation($page, 'rel', 'next') + * $h->findRevSection($page); // $h->findRelation($page, 'rev', 'section'); + * $h->findRelFoo($page); // $h->findRelation($page, 'rel', 'foo'); + * + * + * @param string $method method name + * @param array $arguments method arguments + * @return mixed + * @throws Exception\ExceptionInterface if method does not exist in container + */ + public function __call($method, array $arguments = array()) + { + ErrorHandler::start(E_WARNING); + $result = preg_match('/find(Rel|Rev)(.+)/', $method, $match); + ErrorHandler::stop(); + if ($result) { + return $this->findRelation($arguments[0], + strtolower($match[1]), + strtolower($match[2])); + } + + return parent::__call($method, $arguments); + } + + /** + * Sets the helper's render flag + * + * The helper uses the bitwise '&' operator against the hex values of the + * render constants. This means that the flag can is "bitwised" value of + * the render constants. Examples: + * + * // render all links except glossary + * $flag = Links:RENDER_ALL ^ Links:RENDER_GLOSSARY; + * $helper->setRenderFlag($flag); + * + * // render only chapters and sections + * $flag = Links:RENDER_CHAPTER | Links:RENDER_SECTION; + * $helper->setRenderFlag($flag); + * + * // render only relations that are not native W3C relations + * $helper->setRenderFlag(Links:RENDER_CUSTOM); + * + * // render all relations (default) + * $helper->setRenderFlag(Links:RENDER_ALL); + * + * + * Note that custom relations can also be rendered directly using the + * {@link renderLink()} method. + * + * @param int $renderFlag render flag + * @return Links fluent interface, returns self + */ + public function setRenderFlag($renderFlag) + { + $this->renderFlag = (int) $renderFlag; + return $this; + } + + /** + * Returns the helper's render flag + * + * @return int render flag + */ + public function getRenderFlag() + { + return $this->renderFlag; + } + + // Finder methods: + + /** + * Finds all relations (forward and reverse) for the given $page + * + * The form of the returned array: + * + * // $page denotes an instance of Zend_Navigation_Page + * $returned = array( + * 'rel' => array( + * 'alternate' => array($page, $page, $page), + * 'start' => array($page), + * 'next' => array($page), + * 'prev' => array($page), + * 'canonical' => array($page) + * ), + * 'rev' => array( + * 'section' => array($page) + * ) + * ); + * + * + * @param AbstractPage $page page to find links for + * @return array related pages + */ + public function findAllRelations(AbstractPage $page, $flag = null) + { + if (!is_int($flag)) { + $flag = self::RENDER_ALL; + } + + $result = array('rel' => array(), 'rev' => array()); + $native = array_values(self::$RELATIONS); + + foreach (array_keys($result) as $rel) { + $meth = 'getDefined' . ucfirst($rel); + $types = array_merge($native, array_diff($page->$meth(), $native)); + + foreach ($types as $type) { + if (!$relFlag = array_search($type, self::$RELATIONS)) { + $relFlag = self::RENDER_CUSTOM; + } + if (!($flag & $relFlag)) { + continue; + } + + $found = $this->findRelation($page, $rel, $type); + if ($found) { + if (!is_array($found)) { + $found = array($found); + } + $result[$rel][$type] = $found; + } + } + } + + return $result; + } + + /** + * Finds relations of the given $rel=$type from $page + * + * This method will first look for relations in the page instance, then + * by searching the root container if nothing was found in the page. + * + * @param AbstractPage $page page to find relations for + * @param string $rel relation, "rel" or "rev" + * @param string $type link type, e.g. 'start', 'next' + * @return AbstractPage|array|null page(s), or null if not found + * @throws Exception\DomainException if $rel is not "rel" or "rev" + */ + public function findRelation(AbstractPage $page, $rel, $type) + { + if (!in_array($rel, array('rel', 'rev'))) { + throw new Exception\DomainException(sprintf( + 'Invalid argument: $rel must be "rel" or "rev"; "%s" given', + $rel + )); + } + + if (!$result = $this->findFromProperty($page, $rel, $type)) { + $result = $this->findFromSearch($page, $rel, $type); + } + + return $result; + } + + /** + * Finds relations of given $type for $page by checking if the + * relation is specified as a property of $page + * + * @param AbstractPage $page page to find relations for + * @param string $rel relation, 'rel' or 'rev' + * @param string $type link type, e.g. 'start', 'next' + * @return AbstractPage|array|null page(s), or null if not found + */ + protected function findFromProperty(AbstractPage $page, $rel, $type) + { + $method = 'get' . ucfirst($rel); + $result = $page->$method($type); + if ($result) { + $result = $this->convertToPages($result); + if ($result) { + if (!is_array($result)) { + $result = array($result); + } + + foreach ($result as $key => $page) { + if (!$this->accept($page)) { + unset($result[$key]); + } + } + + return count($result) == 1 ? $result[0] : $result; + } + } + + return null; + } + + /** + * Finds relations of given $rel=$type for $page by using the helper to + * search for the relation in the root container + * + * @param AbstractPage $page page to find relations for + * @param string $rel relation, 'rel' or 'rev' + * @param string $type link type, e.g. 'start', 'next', etc + * @return array|null array of pages, or null if not found + */ + protected function findFromSearch(AbstractPage $page, $rel, $type) + { + $found = null; + + $method = 'search' . ucfirst($rel) . ucfirst($type); + if (method_exists($this, $method)) { + $found = $this->$method($page); + } + + return $found; + } + + // Search methods: + + /** + * Searches the root container for the forward 'start' relation of the given + * $page + * + * From {@link http://www.w3.org/TR/html4/types.html#type-links}: + * Refers to the first document in a collection of documents. This link type + * tells search engines which document is considered by the author to be the + * starting point of the collection. + * + * @param AbstractPage $page page to find relation for + * @return AbstractPage|null page or null + */ + public function searchRelStart(AbstractPage $page) + { + $found = $this->findRoot($page); + if (!$found instanceof AbstractPage) { + $found->rewind(); + $found = $found->current(); + } + + if ($found === $page || !$this->accept($found)) { + $found = null; + } + + return $found; + } + + /** + * Searches the root container for the forward 'next' relation of the given + * $page + * + * From {@link http://www.w3.org/TR/html4/types.html#type-links}: + * Refers to the next document in a linear sequence of documents. User + * agents may choose to preload the "next" document, to reduce the perceived + * load time. + * + * @param AbstractPage $page page to find relation for + * @return AbstractPage|null page(s) or null + */ + public function searchRelNext(AbstractPage $page) + { + $found = null; + $break = false; + $iterator = new RecursiveIteratorIterator($this->findRoot($page), + RecursiveIteratorIterator::SELF_FIRST); + foreach ($iterator as $intermediate) { + if ($intermediate === $page) { + // current page; break at next accepted page + $break = true; + continue; + } + + if ($break && $this->accept($intermediate)) { + $found = $intermediate; + break; + } + } + + return $found; + } + + /** + * Searches the root container for the forward 'prev' relation of the given + * $page + * + * From {@link http://www.w3.org/TR/html4/types.html#type-links}: + * Refers to the previous document in an ordered series of documents. Some + * user agents also support the synonym "Previous". + * + * @param AbstractPage $page page to find relation for + * @return AbstractPage|null page or null + */ + public function searchRelPrev(AbstractPage $page) + { + $found = null; + $prev = null; + $iterator = new RecursiveIteratorIterator( + $this->findRoot($page), + RecursiveIteratorIterator::SELF_FIRST); + foreach ($iterator as $intermediate) { + if (!$this->accept($intermediate)) { + continue; + } + if ($intermediate === $page) { + $found = $prev; + break; + } + + $prev = $intermediate; + } + + return $found; + } + + /** + * Searches the root container for forward 'chapter' relations of the given + * $page + * + * From {@link http://www.w3.org/TR/html4/types.html#type-links}: + * Refers to a document serving as a chapter in a collection of documents. + * + * @param AbstractPage $page page to find relation for + * @return AbstractPage|array|null page(s) or null + */ + public function searchRelChapter(AbstractPage $page) + { + $found = array(); + + // find first level of pages + $root = $this->findRoot($page); + + // find start page(s) + $start = $this->findRelation($page, 'rel', 'start'); + if (!is_array($start)) { + $start = array($start); + } + + foreach ($root as $chapter) { + // exclude self and start page from chapters + if ($chapter !== $page && + !in_array($chapter, $start) && + $this->accept($chapter)) { + $found[] = $chapter; + } + } + + switch (count($found)) { + case 0: + return null; + case 1: + return $found[0]; + default: + return $found; + } + } + + /** + * Searches the root container for forward 'section' relations of the given + * $page + * + * From {@link http://www.w3.org/TR/html4/types.html#type-links}: + * Refers to a document serving as a section in a collection of documents. + * + * @param AbstractPage $page page to find relation for + * @return AbstractPage|array|null page(s) or null + */ + public function searchRelSection(AbstractPage $page) + { + $found = array(); + + // check if given page has pages and is a chapter page + if ($page->hasPages() && $this->findRoot($page)->hasPage($page)) { + foreach ($page as $section) { + if ($this->accept($section)) { + $found[] = $section; + } + } + } + + switch (count($found)) { + case 0: + return null; + case 1: + return $found[0]; + default: + return $found; + } + } + + /** + * Searches the root container for forward 'subsection' relations of the + * given $page + * + * From {@link http://www.w3.org/TR/html4/types.html#type-links}: + * Refers to a document serving as a subsection in a collection of + * documents. + * + * @param AbstractPage $page page to find relation for + * @return AbstractPage|array|null page(s) or null + */ + public function searchRelSubsection(AbstractPage $page) + { + $found = array(); + + if ($page->hasPages()) { + // given page has child pages, loop chapters + foreach ($this->findRoot($page) as $chapter) { + // is page a section? + if ($chapter->hasPage($page)) { + foreach ($page as $subsection) { + if ($this->accept($subsection)) { + $found[] = $subsection; + } + } + } + } + } + + switch (count($found)) { + case 0: + return null; + case 1: + return $found[0]; + default: + return $found; + } + } + + /** + * Searches the root container for the reverse 'section' relation of the + * given $page + * + * From {@link http://www.w3.org/TR/html4/types.html#type-links}: + * Refers to a document serving as a section in a collection of documents. + * + * @param AbstractPage $page page to find relation for + * @return AbstractPage|null page(s) or null + */ + public function searchRevSection(AbstractPage $page) + { + $found = null; + $parent = $page->getParent(); + if ($parent) { + if ($parent instanceof AbstractPage && + $this->findRoot($page)->hasPage($parent)) { + $found = $parent; + } + } + + return $found; + } + + /** + * Searches the root container for the reverse 'section' relation of the + * given $page + * + * From {@link http://www.w3.org/TR/html4/types.html#type-links}: + * Refers to a document serving as a subsection in a collection of + * documents. + * + * @param AbstractPage $page page to find relation for + * @return AbstractPage|null page(s) or null + */ + public function searchRevSubsection(AbstractPage $page) + { + $found = null; + $parent = $page->getParent(); + if ($parent) { + if ($parent instanceof AbstractPage) { + $root = $this->findRoot($page); + foreach ($root as $chapter) { + if ($chapter->hasPage($parent)) { + $found = $parent; + break; + } + } + } + } + + return $found; + } + + // Util methods: + + /** + * Returns the root container of the given page + * + * When rendering a container, the render method still store the given + * container as the root container, and unset it when done rendering. This + * makes sure finder methods will not traverse above the container given + * to the render method. + * + * @param AbstractPage $page page to find root for + * @return AbstractContainer the root container of the given page + */ + protected function findRoot(AbstractPage $page) + { + if ($this->root) { + return $this->root; + } + + $root = $page; + + while ($parent = $page->getParent()) { + $root = $parent; + if ($parent instanceof AbstractPage) { + $page = $parent; + } else { + break; + } + } + + return $root; + } + + /** + * Converts a $mixed value to an array of pages + * + * @param mixed $mixed mixed value to get page(s) from + * @param bool $recursive whether $value should be looped + * if it is an array or a config + * @return AbstractPage|array|null empty if unable to convert + */ + protected function convertToPages($mixed, $recursive = true) + { + if ($mixed instanceof AbstractPage) { + // value is a page instance; return directly + return $mixed; + } elseif ($mixed instanceof AbstractContainer) { + // value is a container; return pages in it + $pages = array(); + foreach ($mixed as $page) { + $pages[] = $page; + } + return $pages; + } elseif ($mixed instanceof Traversable) { + $mixed = ArrayUtils::iteratorToArray($mixed); + } elseif (is_string($mixed)) { + // value is a string; make an URI page + return AbstractPage::factory(array( + 'type' => 'uri', + 'uri' => $mixed + )); + } + + if (is_array($mixed) && !empty($mixed)) { + if ($recursive && is_numeric(key($mixed))) { + // first key is numeric; assume several pages + $pages = array(); + foreach ($mixed as $value) { + $value = $this->convertToPages($value, false); + if ($value) { + $pages[] = $value; + } + } + return $pages; + } else { + // pass array to factory directly + try { + $page = AbstractPage::factory($mixed); + return $page; + } catch (\Exception $e) { + } + } + } + + // nothing found + return null; + } + + // Render methods: + + /** + * Renders the given $page as a link element, with $attrib = $relation + * + * @param AbstractPage $page the page to render the link for + * @param string $attrib the attribute to use for $type, + * either 'rel' or 'rev' + * @param string $relation relation type, muse be one of; + * alternate, appendix, bookmark, + * chapter, contents, copyright, + * glossary, help, home, index, next, + * prev, section, start, stylesheet, + * subsection + * @return string rendered link element + * @throws Exception\DomainException if $attrib is invalid + */ + public function renderLink(AbstractPage $page, $attrib, $relation) + { + if (!in_array($attrib, array('rel', 'rev'))) { + throw new Exception\DomainException(sprintf( + 'Invalid relation attribute "%s", must be "rel" or "rev"', + $attrib + )); + } + + if (!$href = $page->getHref()) { + return ''; + } + + // TODO: add more attribs + // http://www.w3.org/TR/html401/struct/links.html#h-12.2 + $attribs = array( + $attrib => $relation, + 'href' => $href, + 'title' => $page->getLabel() + ); + + return 'htmlAttribs($attribs) . + $this->getClosingBracket(); + } + + // Zend\View\Helper\Navigation\Helper: + + /** + * Renders helper + * + * Implements {@link HelperInterface::render()}. + * + * @param AbstractContainer string|$container [optional] container to render. + * Default is to render the + * container registered in the + * helper. + * @return string helper output + */ + public function render($container = null) + { + $this->parseContainer($container); + if (null === $container) { + $container = $this->getContainer(); + } + + $active = $this->findActive($container); + if ($active) { + $active = $active['page']; + } else { + // no active page + return ''; + } + + $output = ''; + $indent = $this->getIndent(); + $this->root = $container; + + $result = $this->findAllRelations($active, $this->getRenderFlag()); + foreach ($result as $attrib => $types) { + foreach ($types as $relation => $pages) { + foreach ($pages as $page) { + $r = $this->renderLink($page, $attrib, $relation); + if ($r) { + $output .= $indent . $r . self::EOL; + } + } + } + } + + $this->root = null; + + // return output (trim last newline by spec) + return strlen($output) ? rtrim($output, self::EOL) : ''; + } +} diff --git a/src/Helper/Navigation/Menu.php b/src/Helper/Navigation/Menu.php new file mode 100644 index 00000000..de8ab149 --- /dev/null +++ b/src/Helper/Navigation/Menu.php @@ -0,0 +1,659 @@ +setContainer($container); + } + + return $this; + } + + /** + * Sets CSS class to use for the first 'ul' element when rendering + * + * @param string $ulClass CSS class to set + * @return Menu fluent interface, returns self + */ + public function setUlClass($ulClass) + { + if (is_string($ulClass)) { + $this->ulClass = $ulClass; + } + + return $this; + } + + /** + * Returns CSS class to use for the first 'ul' element when rendering + * + * @return string CSS class + */ + public function getUlClass() + { + return $this->ulClass; + } + + /** + * Sets a flag indicating whether only active branch should be rendered + * + * @param bool $flag [optional] render only active branch. Default is true. + * @return Menu fluent interface, returns self + */ + public function setOnlyActiveBranch($flag = true) + { + $this->onlyActiveBranch = (bool) $flag; + return $this; + } + + /** + * Returns a flag indicating whether only active branch should be rendered + * + * By default, this value is false, meaning the entire menu will be + * be rendered. + * + * @return bool whether only active branch should be rendered + */ + public function getOnlyActiveBranch() + { + return $this->onlyActiveBranch; + } + + /** + * Sets a flag indicating whether labels should be escaped + * + * @param bool $flag [optional] escape labels. Default is true. + * @return Menu fluent interface, returns self + */ + public function escapeLabels($flag = true) + { + $this->escapeLabels = (bool) $flag; + return $this; + } + + /** + * Enables/disables rendering of parents when only rendering active branch + * + * See {@link setOnlyActiveBranch()} for more information. + * + * @param bool $flag [optional] render parents when rendering active branch. + * Default is true. + * @return Menu fluent interface, returns self + */ + public function setRenderParents($flag = true) + { + $this->renderParents = (bool) $flag; + return $this; + } + + /** + * Returns flag indicating whether parents should be rendered when rendering + * only the active branch + * + * By default, this value is true. + * + * @return bool whether parents should be rendered + */ + public function getRenderParents() + { + return $this->renderParents; + } + + /** + * Sets which partial view script to use for rendering menu + * + * @param string|array $partial partial view script or null. If an array is + * given, it is expected to contain two + * values; the partial view script to use, + * and the module where the script can be + * found. + * @return Menu fluent interface, returns self + */ + public function setPartial($partial) + { + if (null === $partial || is_string($partial) || is_array($partial)) { + $this->partial = $partial; + } + + return $this; + } + + /** + * Returns partial view script to use for rendering menu + * + * @return string|array|null + */ + public function getPartial() + { + return $this->partial; + } + + // Public methods: + + /** + * Returns an HTML string containing an 'a' element for the given page if + * the page's href is not empty, and a 'span' element if it is empty + * + * Overrides {@link AbstractHelper::htmlify()}. + * + * @param AbstractPage $page page to generate HTML for + * @param bool $escapeLabel Whether or not to escape the label + * @return string HTML string for the given page + */ + public function htmlify(AbstractPage $page, $escapeLabel = true) + { + // get label and title for translating + $label = $page->getLabel(); + $title = $page->getTitle(); + + // translate label and title? + if (null !== ($translator = $this->getTranslator())) { + $textDomain = $this->getTranslatorTextDomain(); + if (is_string($label) && !empty($label)) { + $label = $translator->translate($label, $textDomain); + } + if (is_string($title) && !empty($title)) { + $title = $translator->translate($title, $textDomain); + } + } + + // get attribs for element + $attribs = array( + 'id' => $page->getId(), + 'title' => $title, + 'class' => $page->getClass() + ); + + // does page have a href? + $href = $page->getHref(); + if ($href) { + $element = 'a'; + $attribs['href'] = $href; + $attribs['target'] = $page->getTarget(); + } else { + $element = 'span'; + } + + $html = '<' . $element . $this->htmlAttribs($attribs) . '>'; + if ($escapeLabel === true) { + $escaper = $this->view->plugin('escapeHtml'); + $html .= $escaper($label); + } else { + $html .= $label; + } + $html .= ''; + + return $html; + } + + /** + * Normalizes given render options + * + * @param array $options [optional] options to normalize + * @return array normalized options + */ + protected function normalizeOptions(array $options = array()) + { + if (isset($options['indent'])) { + $options['indent'] = $this->getWhitespace($options['indent']); + } else { + $options['indent'] = $this->getIndent(); + } + + if (isset($options['ulClass']) && $options['ulClass'] !== null) { + $options['ulClass'] = (string) $options['ulClass']; + } else { + $options['ulClass'] = $this->getUlClass(); + } + + if (array_key_exists('minDepth', $options)) { + if (null !== $options['minDepth']) { + $options['minDepth'] = (int) $options['minDepth']; + } + } else { + $options['minDepth'] = $this->getMinDepth(); + } + + if ($options['minDepth'] < 0 || $options['minDepth'] === null) { + $options['minDepth'] = 0; + } + + if (array_key_exists('maxDepth', $options)) { + if (null !== $options['maxDepth']) { + $options['maxDepth'] = (int) $options['maxDepth']; + } + } else { + $options['maxDepth'] = $this->getMaxDepth(); + } + + if (!isset($options['onlyActiveBranch'])) { + $options['onlyActiveBranch'] = $this->getOnlyActiveBranch(); + } + + if (!isset($options['escapeLabels'])) { + $options['escapeLabels'] = $this->escapeLabels; + } + + if (!isset($options['renderParents'])) { + $options['renderParents'] = $this->getRenderParents(); + } + + return $options; + } + + // Render methods: + + /** + * Renders the deepest active menu within [$minDepth, $maxDepth], (called + * from {@link renderMenu()}) + * + * @param AbstractContainer $container container to render + * @param array $active active page and depth + * @param string $ulClass CSS class for first UL + * @param string $indent initial indentation + * @param int|null $minDepth minimum depth + * @param int|null $maxDepth maximum depth + * @return string rendered menu + */ + protected function renderDeepestMenu(AbstractContainer $container, + $ulClass, + $indent, + $minDepth, + $maxDepth, + $escapeLabels + ) { + if (!$active = $this->findActive($container, $minDepth - 1, $maxDepth)) { + return ''; + } + + // special case if active page is one below minDepth + if ($active['depth'] < $minDepth) { + if (!$active['page']->hasPages()) { + return ''; + } + } elseif (!$active['page']->hasPages()) { + // found pages has no children; render siblings + $active['page'] = $active['page']->getParent(); + } elseif (is_int($maxDepth) && $active['depth'] +1 > $maxDepth) { + // children are below max depth; render siblings + $active['page'] = $active['page']->getParent(); + } + + $ulClass = $ulClass ? ' class="' . $ulClass . '"' : ''; + $html = $indent . '' . self::EOL; + + foreach ($active['page'] as $subPage) { + if (!$this->accept($subPage)) { + continue; + } + $liClass = $subPage->isActive(true) ? ' class="active"' : ''; + $html .= $indent . ' ' . self::EOL; + $html .= $indent . ' ' . $this->htmlify($subPage, $escapeLabels) . self::EOL; + $html .= $indent . ' ' . self::EOL; + } + + $html .= $indent . ''; + + return $html; + } + + /** + * Renders a normal menu (called from {@link renderMenu()}) + * + * @param AbstractContainer $container container to render + * @param string $ulClass CSS class for first UL + * @param string $indent initial indentation + * @param int|null $minDepth minimum depth + * @param int|null $maxDepth maximum depth + * @param bool $onlyActive render only active branch? + * @return string + */ + protected function renderNormalMenu(AbstractContainer $container, + $ulClass, + $indent, + $minDepth, + $maxDepth, + $onlyActive, + $escapeLabels + ) { + $html = ''; + + // find deepest active + $found = $this->findActive($container, $minDepth, $maxDepth); + if ($found) { + $foundPage = $found['page']; + $foundDepth = $found['depth']; + } else { + $foundPage = null; + } + + // create iterator + $iterator = new RecursiveIteratorIterator($container, + RecursiveIteratorIterator::SELF_FIRST); + if (is_int($maxDepth)) { + $iterator->setMaxDepth($maxDepth); + } + + // iterate container + $prevDepth = -1; + foreach ($iterator as $page) { + $depth = $iterator->getDepth(); + $isActive = $page->isActive(true); + if ($depth < $minDepth || !$this->accept($page)) { + // page is below minDepth or not accepted by acl/visibility + continue; + } elseif ($onlyActive && !$isActive) { + // page is not active itself, but might be in the active branch + $accept = false; + if ($foundPage) { + if ($foundPage->hasPage($page)) { + // accept if page is a direct child of the active page + $accept = true; + } elseif ($foundPage->getParent()->hasPage($page)) { + // page is a sibling of the active page... + if (!$foundPage->hasPages() || + is_int($maxDepth) && $foundDepth + 1 > $maxDepth) { + // accept if active page has no children, or the + // children are too deep to be rendered + $accept = true; + } + } + } + + if (!$accept) { + continue; + } + } + + // make sure indentation is correct + $depth -= $minDepth; + $myIndent = $indent . str_repeat(' ', $depth); + + if ($depth > $prevDepth) { + // start new ul tag + if ($ulClass && $depth == 0) { + $ulClass = ' class="' . $ulClass . '"'; + } else { + $ulClass = ''; + } + $html .= $myIndent . '' . self::EOL; + } elseif ($prevDepth > $depth) { + // close li/ul tags until we're at current depth + for ($i = $prevDepth; $i > $depth; $i--) { + $ind = $indent . str_repeat(' ', $i); + $html .= $ind . ' ' . self::EOL; + $html .= $ind . '' . self::EOL; + } + // close previous li tag + $html .= $myIndent . ' ' . self::EOL; + } else { + // close previous li tag + $html .= $myIndent . ' ' . self::EOL; + } + + // render li tag and page + $liClass = $isActive ? ' class="active"' : ''; + $html .= $myIndent . ' ' . self::EOL + . $myIndent . ' ' . $this->htmlify($page, $escapeLabels) . self::EOL; + + // store as previous depth for next iteration + $prevDepth = $depth; + } + + if ($html) { + // done iterating container; close open ul/li tags + for ($i = $prevDepth+1; $i > 0; $i--) { + $myIndent = $indent . str_repeat(' ', $i-1); + $html .= $myIndent . ' ' . self::EOL + . $myIndent . '' . self::EOL; + } + $html = rtrim($html, self::EOL); + } + + return $html; + } + + /** + * Renders helper + * + * Renders a HTML 'ul' for the given $container. If $container is not given, + * the container registered in the helper will be used. + * + * Available $options: + * + * + * @param AbstractContainer $container [optional] container to create menu from. + * Default is to use the container retrieved + * from {@link getContainer()}. + * @param array $options [optional] options for controlling rendering + * @return string rendered menu + */ + public function renderMenu($container = null, array $options = array()) + { + $this->parseContainer($container); + if (null === $container) { + $container = $this->getContainer(); + } + + + $options = $this->normalizeOptions($options); + + if ($options['onlyActiveBranch'] && !$options['renderParents']) { + $html = $this->renderDeepestMenu($container, + $options['ulClass'], + $options['indent'], + $options['minDepth'], + $options['maxDepth'], + $options['escapeLabels']); + } else { + $html = $this->renderNormalMenu($container, + $options['ulClass'], + $options['indent'], + $options['minDepth'], + $options['maxDepth'], + $options['onlyActiveBranch'], + $options['escapeLabels']); + } + + return $html; + } + + /** + * Renders the inner-most sub menu for the active page in the $container + * + * This is a convenience method which is equivalent to the following call: + * + * renderMenu($container, array( + * 'indent' => $indent, + * 'ulClass' => $ulClass, + * 'minDepth' => null, + * 'maxDepth' => null, + * 'onlyActiveBranch' => true, + * 'renderParents' => false + * )); + * + * + * @param AbstractContainer $container [optional] container to + * render. Default is to render + * the container registered in + * the helper. + * @param string $ulClass [optional] CSS class to + * use for UL element. Default + * is to use the value from + * {@link getUlClass()}. + * @param string|int $indent [optional] indentation as + * a string or number of + * spaces. Default is to use + * the value retrieved from + * {@link getIndent()}. + * @return string rendered content + */ + public function renderSubMenu(AbstractContainer $container = null, + $ulClass = null, + $indent = null + ) { + return $this->renderMenu($container, array( + 'indent' => $indent, + 'ulClass' => $ulClass, + 'minDepth' => null, + 'maxDepth' => null, + 'onlyActiveBranch' => true, + 'renderParents' => false, + 'escapeLabels' => true + )); + } + + /** + * Renders the given $container by invoking the partial view helper + * + * The container will simply be passed on as a model to the view script + * as-is, and will be available in the partial script as 'container', e.g. + * echo 'Number of pages: ', count($this->container);. + * + * @param AbstractContainer $container [optional] container to pass to view + * script. Default is to use the container + * registered in the helper. + * @param string|array $partial [optional] partial view script to use. + * Default is to use the partial + * registered in the helper. If an array + * is given, it is expected to contain two + * values; the partial view script to use, + * and the module where the script can be + * found. + * @return string helper output + * @throws Exception\RuntimeException if no partial provided + * @throws Exception\InvalidArgumentException if partial is invalid array + */ + public function renderPartial($container = null, $partial = null) + { + $this->parseContainer($container); + if (null === $container) { + $container = $this->getContainer(); + } + + if (null === $partial) { + $partial = $this->getPartial(); + } + + if (empty($partial)) { + throw new Exception\RuntimeException( + 'Unable to render menu: No partial view script provided' + ); + } + + $model = array( + 'container' => $container + ); + + if (is_array($partial)) { + if (count($partial) != 2) { + throw new Exception\InvalidArgumentException( + 'Unable to render menu: A view partial supplied as ' + . 'an array must contain two values: partial view ' + . 'script and module where script can be found' + ); + } + + $partialHelper = $this->view->plugin('partial'); + return $partialHelper($partial[0], /*$partial[1], */$model); + } + + $partialHelper = $this->view->plugin('partial'); + return $partialHelper($partial, $model); + } + + // Zend\View\Helper\Navigation\Helper: + + /** + * Renders menu + * + * Implements {@link HelperInterface::render()}. + * + * If a partial view is registered in the helper, the menu will be rendered + * using the given partial script. If no partial is registered, the menu + * will be rendered as an 'ul' element by the helper's internal method. + * + * @see renderPartial() + * @see renderMenu() + * + * @param AbstractContainer $container [optional] container to render. Default is + * to render the container registered in the + * helper. + * @return string helper output + */ + public function render($container = null) + { + $partial = $this->getPartial(); + if ($partial) { + return $this->renderPartial($container, $partial); + } + return $this->renderMenu($container); + } +} diff --git a/src/Helper/Navigation/PluginManager.php b/src/Helper/Navigation/PluginManager.php new file mode 100644 index 00000000..c743bb8c --- /dev/null +++ b/src/Helper/Navigation/PluginManager.php @@ -0,0 +1,63 @@ + 'Zend\View\Helper\Navigation\Breadcrumbs', + 'links' => 'Zend\View\Helper\Navigation\Links', + 'menu' => 'Zend\View\Helper\Navigation\Menu', + 'sitemap' => 'Zend\View\Helper\Navigation\Sitemap', + ); + + /** + * Validate the plugin + * + * Checks that the helper loaded is an instance of AbstractHelper. + * + * @param mixed $plugin + * @return void + * @throws Exception\InvalidArgumentException if invalid + */ + public function validatePlugin($plugin) + { + if ($plugin instanceof AbstractHelper) { + // we're okay + return; + } + + throw new Exception\InvalidArgumentException(sprintf( + 'Plugin of type %s is invalid; must implement %s\AbstractHelper', + (is_object($plugin) ? get_class($plugin) : gettype($plugin)), + __NAMESPACE__ + )); + } +} diff --git a/src/Helper/Navigation/Sitemap.php b/src/Helper/Navigation/Sitemap.php new file mode 100644 index 00000000..a4e85a94 --- /dev/null +++ b/src/Helper/Navigation/Sitemap.php @@ -0,0 +1,461 @@ + tag + * + * @var string + */ + const SITEMAP_NS = 'http://www.sitemaps.org/schemas/sitemap/0.9'; + + /** + * Schema URL + * + * @var string + */ + const SITEMAP_XSD = 'http://www.sitemaps.org/schemas/sitemap/0.9/sitemap.xsd'; + + /** + * Whether XML output should be formatted + * + * @var bool + */ + protected $formatOutput = false; + + /** + * Whether the XML declaration should be included in XML output + * + * @var bool + */ + protected $useXmlDeclaration = true; + + /** + * Whether sitemap should be validated using Zend\Validate\Sitemap\* + * + * @var bool + */ + protected $useSitemapValidators = true; + + /** + * Whether sitemap should be schema validated when generated + * + * @var bool + */ + protected $useSchemaValidation = false; + + /** + * Server url + * + * @var string + */ + protected $serverUrl; + + /** + * List of urls in the sitemap + * + * @var array + */ + protected $urls = array(); + + /** + * Helper entry point + * + * @param string|AbstractContainer $container container to operate on + * @return Navigation + */ + public function __invoke($container = null) + { + if (null !== $container) { + $this->setContainer($container); + } + + return $this; + } + + /** + * Sets whether XML output should be formatted + * + * @param bool $formatOutput [optional] whether output should be formatted. Default is true. + * @return Sitemap fluent interface, returns self + */ + public function setFormatOutput($formatOutput = true) + { + $this->formatOutput = (bool) $formatOutput; + return $this; + } + + /** + * Returns whether XML output should be formatted + * + * @return bool whether XML output should be formatted + */ + public function getFormatOutput() + { + return $this->formatOutput; + } + + /** + * Sets whether the XML declaration should be used in output + * + * @param bool $useXmlDecl whether XML declaration should be rendered + * @return Sitemap fluent interface, returns self + */ + public function setUseXmlDeclaration($useXmlDecl) + { + $this->useXmlDeclaration = (bool) $useXmlDecl; + return $this; + } + + /** + * Returns whether the XML declaration should be used in output + * + * @return bool whether the XML declaration should be used in output + */ + public function getUseXmlDeclaration() + { + return $this->useXmlDeclaration; + } + + /** + * Sets whether sitemap should be validated using Zend\Validate\Sitemap_* + * + * @param bool $useSitemapValidators whether sitemap validators should be used + * @return Sitemap fluent interface, returns self + */ + public function setUseSitemapValidators($useSitemapValidators) + { + $this->useSitemapValidators = (bool) $useSitemapValidators; + return $this; + } + + /** + * Returns whether sitemap should be validated using Zend\Validate\Sitemap_* + * + * @return bool whether sitemap should be validated using validators + */ + public function getUseSitemapValidators() + { + return $this->useSitemapValidators; + } + + /** + * Sets whether sitemap should be schema validated when generated + * + * @param bool $schemaValidation whether sitemap should validated using XSD Schema + * @return Sitemap + */ + public function setUseSchemaValidation($schemaValidation) + { + $this->useSchemaValidation = (bool) $schemaValidation; + return $this; + } + + /** + * Returns true if sitemap should be schema validated when generated + * + * @return bool + */ + public function getUseSchemaValidation() + { + return $this->useSchemaValidation; + } + + /** + * Sets server url (scheme and host-related stuff without request URI) + * + * E.g. http://www.example.com + * + * @param string $serverUrl server URL to set (only scheme and host) + * @return Sitemap fluent interface, returns self + * @throws Exception\InvalidArgumentException if invalid server URL + */ + public function setServerUrl($serverUrl) + { + $uri = Uri\UriFactory::factory($serverUrl); + $uri->setFragment(''); + $uri->setPath(''); + $uri->setQuery(''); + + if ($uri->isValid()) { + $this->serverUrl = $uri->toString(); + } else { + throw new Exception\InvalidArgumentException(sprintf( + 'Invalid server URL: "%s"', + $serverUrl + )); + } + + return $this; + } + + /** + * Returns server URL + * + * @return string server URL + */ + public function getServerUrl() + { + if (!isset($this->serverUrl)) { + $serverUrlHelper = $this->getView()->plugin('serverUrl'); + $this->serverUrl = $serverUrlHelper(); + } + + return $this->serverUrl; + } + + // Helper methods: + + /** + * Escapes string for XML usage + * + * @param string $string string to escape + * @return string escaped string + */ + protected function xmlEscape($string) + { + $enc = 'UTF-8'; + if ($this->view instanceof View\Renderer\RendererInterface + && method_exists($this->view, 'getEncoding') + ) { + $enc = $this->view->getEncoding(); + } + + return htmlspecialchars($string, ENT_QUOTES, $enc, false); + } + + // Public methods: + + /** + * Returns an escaped absolute URL for the given page + * + * @param AbstractPage $page page to get URL from + * @return string + */ + public function url(AbstractPage $page) + { + $href = $page->getHref(); + + if (!isset($href{0})) { + // no href + return ''; + } elseif ($href{0} == '/') { + // href is relative to root; use serverUrl helper + $url = $this->getServerUrl() . $href; + } elseif (preg_match('/^[a-z]+:/im', (string) $href)) { + // scheme is given in href; assume absolute URL already + $url = (string) $href; + } else { + // href is relative to current document; use url helpers + $basePathHelper = $this->getView()->plugin('basepath'); + $curDoc = $basePathHelper(); + $curDoc = ('/' == $curDoc) ? '' : trim($curDoc, '/'); + $url = rtrim($this->getServerUrl(), '/') . '/' + . $curDoc + . (empty($curDoc) ? '' : '/') . $href; + } + + if (! in_array($url, $this->urls)) { + + $this->urls[] = $url; + return $this->xmlEscape($url); + } + + return null; + } + + /** + * Returns a DOMDocument containing the Sitemap XML for the given container + * + * @param AbstractContainer $container [optional] container to get + * breadcrumbs from, defaults + * to what is registered in the + * helper + * @return DOMDocument DOM representation of the + * container + * @throws Exception\RuntimeException if schema validation is on + * and the sitemap is invalid + * according to the sitemap + * schema, or if sitemap + * validators are used and the + * loc element fails validation + */ + public function getDomSitemap(AbstractContainer $container = null) + { + // Reset the urls + $this->urls = array(); + + if (null === $container) { + $container = $this->getContainer(); + } + + // check if we should validate using our own validators + if ($this->getUseSitemapValidators()) { + // create validators + $locValidator = new \Zend\Validator\Sitemap\Loc(); + $lastmodValidator = new \Zend\Validator\Sitemap\Lastmod(); + $changefreqValidator = new \Zend\Validator\Sitemap\Changefreq(); + $priorityValidator = new \Zend\Validator\Sitemap\Priority(); + } + + // create document + $dom = new DOMDocument('1.0', 'UTF-8'); + $dom->formatOutput = $this->getFormatOutput(); + + // ...and urlset (root) element + $urlSet = $dom->createElementNS(self::SITEMAP_NS, 'urlset'); + $dom->appendChild($urlSet); + + // create iterator + $iterator = new RecursiveIteratorIterator($container, + RecursiveIteratorIterator::SELF_FIRST); + + $maxDepth = $this->getMaxDepth(); + if (is_int($maxDepth)) { + $iterator->setMaxDepth($maxDepth); + } + $minDepth = $this->getMinDepth(); + if (!is_int($minDepth) || $minDepth < 0) { + $minDepth = 0; + } + + // iterate container + foreach ($iterator as $page) { + if ($iterator->getDepth() < $minDepth || !$this->accept($page)) { + // page should not be included + continue; + } + + // get absolute url from page + if (!$url = $this->url($page)) { + // skip page if it has no url (rare case) + // or already is in the sitemap + continue; + } + + // create url node for this page + $urlNode = $dom->createElementNS(self::SITEMAP_NS, 'url'); + $urlSet->appendChild($urlNode); + + if ($this->getUseSitemapValidators() + && !$locValidator->isValid($url) + ) { + throw new Exception\RuntimeException(sprintf( + 'Encountered an invalid URL for Sitemap XML: "%s"', + $url + )); + } + + // put url in 'loc' element + $urlNode->appendChild($dom->createElementNS(self::SITEMAP_NS, + 'loc', $url)); + + // add 'lastmod' element if a valid lastmod is set in page + if (isset($page->lastmod)) { + $lastmod = strtotime((string) $page->lastmod); + + // prevent 1970-01-01... + if ($lastmod !== false) { + $lastmod = date('c', $lastmod); + } + + if (!$this->getUseSitemapValidators() || + $lastmodValidator->isValid($lastmod)) { + $urlNode->appendChild( + $dom->createElementNS(self::SITEMAP_NS, 'lastmod', + $lastmod) + ); + } + } + + // add 'changefreq' element if a valid changefreq is set in page + if (isset($page->changefreq)) { + $changefreq = $page->changefreq; + if (!$this->getUseSitemapValidators() || + $changefreqValidator->isValid($changefreq)) { + $urlNode->appendChild( + $dom->createElementNS(self::SITEMAP_NS, 'changefreq', + $changefreq) + ); + } + } + + // add 'priority' element if a valid priority is set in page + if (isset($page->priority)) { + $priority = $page->priority; + if (!$this->getUseSitemapValidators() || + $priorityValidator->isValid($priority)) { + $urlNode->appendChild( + $dom->createElementNS(self::SITEMAP_NS, 'priority', + $priority) + ); + } + } + } + + // validate using schema if specified + if ($this->getUseSchemaValidation()) { + ErrorHandler::start(); + $test = $dom->schemaValidate(self::SITEMAP_XSD); + $error = ErrorHandler::stop(); + if (!$test) { + throw new Exception\RuntimeException(sprintf( + 'Sitemap is invalid according to XML Schema at "%s"', + self::SITEMAP_XSD + ), 0, $error); + } + } + + return $dom; + } + + // Zend_View_Helper_Navigation_Helper: + + /** + * Renders helper + * + * Implements {@link HelperInterface::render()}. + * + * @param link|AbstractContainer $container [optional] container to render. Default is + * to render the container registered in the + * helper. + * @return string helper output + */ + public function render($container = null) + { + $dom = $this->getDomSitemap($container); + $xml = $this->getUseXmlDeclaration() ? + $dom->saveXML() : + $dom->saveXML($dom->documentElement); + + return rtrim($xml, PHP_EOL); + } +} diff --git a/src/Helper/PaginationControl.php b/src/Helper/PaginationControl.php new file mode 100644 index 00000000..0bf6093a --- /dev/null +++ b/src/Helper/PaginationControl.php @@ -0,0 +1,136 @@ +paginator is set and, + * if so, uses that. Also, if no scrolling style or partial are specified, + * the defaults will be used (if set). + * + * @param \Zend\Paginator\Paginator (Optional) $paginator + * @param string $scrollingStyle (Optional) Scrolling style + * @param string $partial (Optional) View partial + * @param array|string $params (Optional) params to pass to the partial + * @return string + * @throws Exception\RuntimeException if no paginator or no view partial provided + * @throws Exception\InvalidArgumentException if partial is invalid array + */ + public function __invoke(Paginator\Paginator $paginator = null, $scrollingStyle = null, $partial = null, $params = null) + { + if ($paginator === null) { + if (isset($this->view->paginator) and $this->view->paginator !== null and $this->view->paginator instanceof Paginator\Paginator) { + $paginator = $this->view->paginator; + } else { + throw new Exception\RuntimeException('No paginator instance provided or incorrect type'); + } + } + + if ($partial === null) { + if (self::$defaultViewPartial === null) { + throw new Exception\RuntimeException('No view partial provided and no default set'); + } + + $partial = self::$defaultViewPartial; + } + + if ($scrollingStyle === null) { + $scrollingStyle = self::$defaultScrollingStyle; + } + + $pages = get_object_vars($paginator->getPages($scrollingStyle)); + + if ($params !== null) { + $pages = array_merge($pages, (array) $params); + } + + if (is_array($partial)) { + if (count($partial) != 2) { + throw new Exception\InvalidArgumentException( + 'A view partial supplied as an array must contain two values: the filename and its module' + ); + } + + if ($partial[1] !== null) { + $partialHelper = $this->view->plugin('partial'); + return $partialHelper($partial[0], $pages); + } + + $partial = $partial[0]; + } + + $partialHelper = $this->view->plugin('partial'); + return $partialHelper($partial, $pages); + } +} diff --git a/src/Helper/Partial.php b/src/Helper/Partial.php new file mode 100644 index 00000000..ede7ca14 --- /dev/null +++ b/src/Helper/Partial.php @@ -0,0 +1,117 @@ +cloneView(); + if (isset($this->partialCounter)) { + $view->partialCounter = $this->partialCounter; + } + + if (!empty($model)) { + if (is_array($model)) { + $view->vars()->assign($model); + } elseif (is_object($model)) { + if (null !== ($objectKey = $this->getObjectKey())) { + $view->vars()->offsetSet($objectKey, $model); + } elseif (method_exists($model, 'toArray')) { + $view->vars()->assign($model->toArray()); + } else { + $view->vars()->assign(get_object_vars($model)); + } + } + } + + return $view->render($name); + } + + /** + * Clone the current View + * + * @return \Zend\View\Renderer\RendererInterface + */ + public function cloneView() + { + $view = clone $this->view; + $view->setVars(array()); + return $view; + } + + /** + * Set object key + * + * @param string $key + * @return Partial + */ + public function setObjectKey($key) + { + if (null === $key) { + $this->objectKey = null; + } else { + $this->objectKey = (string) $key; + } + + return $this; + } + + /** + * Retrieve object key + * + * The objectKey is the variable to which an object in the iterator will be + * assigned. + * + * @return null|string + */ + public function getObjectKey() + { + return $this->objectKey; + } +} diff --git a/src/Helper/PartialLoop.php b/src/Helper/PartialLoop.php new file mode 100644 index 00000000..03c4bc0e --- /dev/null +++ b/src/Helper/PartialLoop.php @@ -0,0 +1,75 @@ +toArray(); + } + + $content = ''; + // reset the counter if it's called again + $this->partialCounter = 0; + foreach ($model as $item) { + // increment the counter variable + $this->partialCounter++; + + $content .= parent::__invoke($name, $item); + } + + return $content; + } +} diff --git a/src/Helper/Placeholder.php b/src/Helper/Placeholder.php new file mode 100644 index 00000000..91fda876 --- /dev/null +++ b/src/Helper/Placeholder.php @@ -0,0 +1,74 @@ +registry = Placeholder\Registry::getRegistry(); + } + + /** + * Placeholder helper + * + * @param string $name + * @return \Zend\View\Helper\Placeholder\Container\AbstractContainer + * @throws InvalidArgumentException + */ + public function __invoke($name = null) + { + if ($name == null) { + throw new InvalidArgumentException('Placeholder: missing argument. $name is required by placeholder($name)'); + } + + $name = (string) $name; + return $this->registry->getContainer($name); + } + + /** + * Retrieve the registry + * + * @return \Zend\View\Helper\Placeholder\Registry + */ + public function getRegistry() + { + return $this->registry; + } +} diff --git a/src/Helper/Placeholder/Container.php b/src/Helper/Placeholder/Container.php new file mode 100644 index 00000000..03f9f1f1 --- /dev/null +++ b/src/Helper/Placeholder/Container.php @@ -0,0 +1,21 @@ +exchangeArray(array($value)); + } + + /** + * Prepend a value to the top of the container + * + * @param mixed $value + * @return void + */ + public function prepend($value) + { + $values = $this->getArrayCopy(); + array_unshift($values, $value); + $this->exchangeArray($values); + } + + /** + * Retrieve container value + * + * If single element registered, returns that element; otherwise, + * serializes to array. + * + * @return mixed + */ + public function getValue() + { + if (1 == count($this)) { + $keys = $this->getKeys(); + $key = array_shift($keys); + return $this[$key]; + } + + return $this->getArrayCopy(); + } + + /** + * Set prefix for __toString() serialization + * + * @param string $prefix + * @return \Zend\View\Helper\Placeholder\Container\AbstractContainer + */ + public function setPrefix($prefix) + { + $this->prefix = (string) $prefix; + return $this; + } + + /** + * Retrieve prefix + * + * @return string + */ + public function getPrefix() + { + return $this->prefix; + } + + /** + * Set postfix for __toString() serialization + * + * @param string $postfix + * @return \Zend\View\Helper\Placeholder\Container\AbstractContainer + */ + public function setPostfix($postfix) + { + $this->postfix = (string) $postfix; + return $this; + } + + /** + * Retrieve postfix + * + * @return string + */ + public function getPostfix() + { + return $this->postfix; + } + + /** + * Set separator for __toString() serialization + * + * Used to implode elements in container + * + * @param string $separator + * @return \Zend\View\Helper\Placeholder\Container\AbstractContainer + */ + public function setSeparator($separator) + { + $this->separator = (string) $separator; + return $this; + } + + /** + * Retrieve separator + * + * @return string + */ + public function getSeparator() + { + return $this->separator; + } + + /** + * Set the indentation string for __toString() serialization, + * optionally, if a number is passed, it will be the number of spaces + * + * @param string|int $indent + * @return \Zend\View\Helper\Placeholder\Container\AbstractContainer + */ + public function setIndent($indent) + { + $this->indent = $this->getWhitespace($indent); + return $this; + } + + /** + * Retrieve indentation + * + * @return string + */ + public function getIndent() + { + return $this->indent; + } + + /** + * Retrieve whitespace representation of $indent + * + * @param int|string $indent + * @return string + */ + public function getWhitespace($indent) + { + if (is_int($indent)) { + $indent = str_repeat(' ', $indent); + } + + return (string) $indent; + } + + /** + * Start capturing content to push into placeholder + * + * @param int $type How to capture content into placeholder; append, prepend, or set + * @return void + * @throws Exception\RuntimeException if nested captures detected + */ + public function captureStart($type = AbstractContainer::APPEND, $key = null) + { + if ($this->captureLock) { + throw new Exception\RuntimeException( + 'Cannot nest placeholder captures for the same placeholder' + ); + } + + $this->captureLock = true; + $this->captureType = $type; + if ((null !== $key) && is_scalar($key)) { + $this->captureKey = (string) $key; + } + ob_start(); + } + + /** + * End content capture + * + * @return void + */ + public function captureEnd() + { + $data = ob_get_clean(); + $key = null; + $this->captureLock = false; + if (null !== $this->captureKey) { + $key = $this->captureKey; + } + switch ($this->captureType) { + case self::SET: + if (null !== $key) { + $this[$key] = $data; + } else { + $this->exchangeArray(array($data)); + } + break; + case self::PREPEND: + if (null !== $key) { + $array = array($key => $data); + $values = $this->getArrayCopy(); + $final = $array + $values; + $this->exchangeArray($final); + } else { + $this->prepend($data); + } + break; + case self::APPEND: + default: + if (null !== $key) { + if (empty($this[$key])) { + $this[$key] = $data; + } else { + $this[$key] .= $data; + } + } else { + $this[$this->nextIndex()] = $data; + } + break; + } + } + + /** + * Get keys + * + * @return array + */ + public function getKeys() + { + $array = $this->getArrayCopy(); + return array_keys($array); + } + + /** + * Next Index + * + * as defined by the PHP manual + * @return int + */ + public function nextIndex() + { + $keys = $this->getKeys(); + if (0 == count($keys)) { + return 0; + } + + return $nextIndex = max($keys) + 1; + } + + /** + * Render the placeholder + * + * @return string + */ + public function toString($indent = null) + { + $indent = ($indent !== null) + ? $this->getWhitespace($indent) + : $this->getIndent(); + + $items = $this->getArrayCopy(); + $return = $indent + . $this->getPrefix() + . implode($this->getSeparator(), $items) + . $this->getPostfix(); + $return = preg_replace("/(\r\n?|\n)/", '$1' . $indent, $return); + return $return; + } + + /** + * Serialize object to string + * + * @return string + */ + public function __toString() + { + return $this->toString(); + } +} diff --git a/src/Helper/Placeholder/Container/AbstractStandalone.php b/src/Helper/Placeholder/Container/AbstractStandalone.php new file mode 100644 index 00000000..105cb022 --- /dev/null +++ b/src/Helper/Placeholder/Container/AbstractStandalone.php @@ -0,0 +1,317 @@ +setRegistry(Registry::getRegistry()); + $this->setContainer($this->getRegistry()->getContainer($this->regKey)); + } + + /** + * Retrieve registry + * + * @return \Zend\View\Helper\Placeholder\Registry + */ + public function getRegistry() + { + return $this->registry; + } + + /** + * Set registry object + * + * @param \Zend\View\Helper\Placeholder\Registry $registry + * @return \Zend\View\Helper\Placeholder\Container\AbstractStandalone + */ + public function setRegistry(Registry $registry) + { + $this->registry = $registry; + return $this; + } + + /** + * Set whether or not auto escaping should be used + * + * @param bool $autoEscape whether or not to auto escape output + * @return \Zend\View\Helper\Placeholder\Container\AbstractStandalone + */ + public function setAutoEscape($autoEscape = true) + { + $this->autoEscape = ($autoEscape) ? true : false; + return $this; + } + + /** + * Return whether autoEscaping is enabled or disabled + * + * return bool + */ + public function getAutoEscape() + { + return $this->autoEscape; + } + + /** + * Escape a string + * + * @param string $string + * @return string + */ + protected function escape($string) + { + $enc = 'UTF-8'; + if ($this->view instanceof \Zend\View\Renderer\RendererInterface + && method_exists($this->view, 'getEncoding') + ) { + $enc = $this->view->getEncoding(); + $escaper = $this->view->plugin('escapeHtml'); + return $escaper((string) $string); + } + /** + * bump this out to a protected method to kill the instance penalty! + */ + $escaper = new \Zend\Escaper\Escaper($enc); + return $escaper->escapeHtml((string) $string); + /** + * Replaced to ensure consistent escaping + */ + //return htmlspecialchars((string) $string, ENT_COMPAT, $enc); + } + + /** + * Set container on which to operate + * + * @param \Zend\View\Helper\Placeholder\Container\AbstractContainer $container + * @return \Zend\View\Helper\Placeholder\Container\AbstractStandalone + */ + public function setContainer(AbstractContainer $container) + { + $this->container = $container; + return $this; + } + + /** + * Retrieve placeholder container + * + * @return \Zend\View\Helper\Placeholder\Container\AbstractContainer + */ + public function getContainer() + { + return $this->container; + } + + /** + * Overloading: set property value + * + * @param string $key + * @param mixed $value + * @return void + */ + public function __set($key, $value) + { + $container = $this->getContainer(); + $container[$key] = $value; + } + + /** + * Overloading: retrieve property + * + * @param string $key + * @return mixed + */ + public function __get($key) + { + $container = $this->getContainer(); + if (isset($container[$key])) { + return $container[$key]; + } + + return null; + } + + /** + * Overloading: check if property is set + * + * @param string $key + * @return bool + */ + public function __isset($key) + { + $container = $this->getContainer(); + return isset($container[$key]); + } + + /** + * Overloading: unset property + * + * @param string $key + * @return void + */ + public function __unset($key) + { + $container = $this->getContainer(); + if (isset($container[$key])) { + unset($container[$key]); + } + } + + /** + * Overload + * + * Proxy to container methods + * + * @param string $method + * @param array $args + * @return mixed + * @throws Exception\BadMethodCallException + */ + public function __call($method, $args) + { + $container = $this->getContainer(); + if (method_exists($container, $method)) { + $return = call_user_func_array(array($container, $method), $args); + if ($return === $container) { + // If the container is returned, we really want the current object + return $this; + } + return $return; + } + + throw new Exception\BadMethodCallException('Method "' . $method . '" does not exist'); + } + + /** + * String representation + * + * @return string + */ + public function toString() + { + return $this->getContainer()->toString(); + } + + /** + * Cast to string representation + * + * @return string + */ + public function __toString() + { + return $this->toString(); + } + + /** + * Countable + * + * @return int + */ + public function count() + { + $container = $this->getContainer(); + return count($container); + } + + /** + * ArrayAccess: offsetExists + * + * @param string|int $offset + * @return bool + */ + public function offsetExists($offset) + { + return $this->getContainer()->offsetExists($offset); + } + + /** + * ArrayAccess: offsetGet + * + * @param string|int $offset + * @return mixed + */ + public function offsetGet($offset) + { + return $this->getContainer()->offsetGet($offset); + } + + /** + * ArrayAccess: offsetSet + * + * @param string|int $offset + * @param mixed $value + * @return void + */ + public function offsetSet($offset, $value) + { + return $this->getContainer()->offsetSet($offset, $value); + } + + /** + * ArrayAccess: offsetUnset + * + * @param string|int $offset + * @return void + */ + public function offsetUnset($offset) + { + return $this->getContainer()->offsetUnset($offset); + } + + /** + * IteratorAggregate: get Iterator + * + * @return Iterator + */ + public function getIterator() + { + return $this->getContainer()->getIterator(); + } +} diff --git a/src/Helper/Placeholder/Registry.php b/src/Helper/Placeholder/Registry.php new file mode 100644 index 00000000..78aa7958 --- /dev/null +++ b/src/Helper/Placeholder/Registry.php @@ -0,0 +1,178 @@ +items[$key] = new $this->containerClass($value); + return $this->items[$key]; + } + + /** + * Retrieve a placeholder container + * + * @param string $key + * @return Container\AbstractContainer + */ + public function getContainer($key) + { + $key = (string) $key; + if (isset($this->items[$key])) { + return $this->items[$key]; + } + + $container = $this->createContainer($key); + + return $container; + } + + /** + * Does a particular container exist? + * + * @param string $key + * @return bool + */ + public function containerExists($key) + { + $key = (string) $key; + $return = array_key_exists($key, $this->items); + return $return; + } + + /** + * Set the container for an item in the registry + * + * @param string $key + * @param Container\AbstractContainer $container + * @return Registry + */ + public function setContainer($key, Container\AbstractContainer $container) + { + $key = (string) $key; + $this->items[$key] = $container; + return $this; + } + + /** + * Delete a container + * + * @param string $key + * @return bool + */ + public function deleteContainer($key) + { + $key = (string) $key; + if (isset($this->items[$key])) { + unset($this->items[$key]); + return true; + } + + return false; + } + + /** + * Set the container class to use + * + * @param string $name + * @throws Exception\InvalidArgumentException + * @throws Exception\DomainException + * @return Registry + */ + public function setContainerClass($name) + { + if (!class_exists($name)) { + throw new Exception\DomainException( + sprintf('%s expects a valid registry class name; received "%s", which did not resolve', + __METHOD__, + $name + )); + } + + if (!in_array('Zend\View\Helper\Placeholder\Container\AbstractContainer', class_parents($name))) { + throw new Exception\InvalidArgumentException('Invalid Container class specified'); + } + + $this->containerClass = $name; + return $this; + } + + /** + * Retrieve the container class + * + * @return string + */ + public function getContainerClass() + { + return $this->containerClass; + } +} diff --git a/src/Helper/RenderChildModel.php b/src/Helper/RenderChildModel.php new file mode 100644 index 00000000..f597922b --- /dev/null +++ b/src/Helper/RenderChildModel.php @@ -0,0 +1,126 @@ +render($child); + } + + /** + * Render a model + * + * If a matching child model is found, it is rendered. If not, an empty + * string is returned. + * + * @param string $child + * @return string + */ + public function render($child) + { + $model = $this->findChild($child); + if (!$model) { + return ''; + } + + $current = $this->current; + $view = $this->getView(); + $return = $view->render($model); + $helper = $this->getViewModelHelper(); + $helper->setCurrent($current); + return $return; + } + + /** + * Find the named child model + * + * Iterates through the current view model, looking for a child model that + * has a captureTo value matching the requested $child. If found, that child + * model is returned; otherwise, a boolean false is returned. + * + * @param string $child + * @return false|Model + */ + protected function findChild($child) + { + $this->current = $model = $this->getCurrent(); + foreach ($model->getChildren() as $childModel) { + if ($childModel->captureTo() == $child) { + return $childModel; + } + } + return false; + } + + /** + * Get the current view model + * + * @return null|Model + */ + protected function getCurrent() + { + $helper = $this->getViewModelHelper(); + if (!$helper->hasCurrent()) { + throw new Exception\RuntimeException(sprintf( + '%s: no view model currently registered in renderer; cannot query for children', + __METHOD__ + )); + } + return $helper->getCurrent(); + } + + /** + * Retrieve the view model helper + * + * @return ViewModel + */ + protected function getViewModelHelper() + { + if ($this->viewModelHelper) { + return $this->viewModelHelper; + } + $view = $this->getView(); + $this->viewModelHelper = $view->plugin('view_model'); + return $this->viewModelHelper; + } +} diff --git a/src/Helper/RenderToPlaceholder.php b/src/Helper/RenderToPlaceholder.php new file mode 100644 index 00000000..1be01f1e --- /dev/null +++ b/src/Helper/RenderToPlaceholder.php @@ -0,0 +1,37 @@ +view->plugin('placeholder'); + $placeholderHelper($placeholder)->captureStart(); + echo $this->view->render($script); + $placeholderHelper($placeholder)->captureEnd(); + } +} diff --git a/src/Helper/ServerUrl.php b/src/Helper/ServerUrl.php new file mode 100644 index 00000000..45d0e81b --- /dev/null +++ b/src/Helper/ServerUrl.php @@ -0,0 +1,142 @@ +setScheme($scheme); + + if (isset($_SERVER['HTTP_X_FORWARDED_HOST']) && !empty($_SERVER['HTTP_X_FORWARDED_HOST'])) { + $host = $_SERVER['HTTP_X_FORWARDED_HOST']; + if (strpos($host, ',') !== false) { + $hosts = explode(',', $host); + $host = trim(array_pop($hosts)); + } + $this->setHost($host); + } elseif (isset($_SERVER['HTTP_HOST']) && !empty($_SERVER['HTTP_HOST'])) { + $this->setHost($_SERVER['HTTP_HOST']); + } elseif (isset($_SERVER['SERVER_NAME'], $_SERVER['SERVER_PORT'])) { + $name = $_SERVER['SERVER_NAME']; + $port = $_SERVER['SERVER_PORT']; + + if (($scheme == 'http' && $port == 80) || + ($scheme == 'https' && $port == 443)) { + $this->setHost($name); + } else { + $this->setHost($name . ':' . $port); + } + } + } + + /** + * View helper entry point: + * Returns the current host's URL like http://site.com + * + * @param string|boolean $requestUri [optional] if true, the request URI + * found in $_SERVER will be appended + * as a path. If a string is given, it + * will be appended as a path. Default + * is to not append any path. + * @return string server url + */ + public function __invoke($requestUri = null) + { + if ($requestUri === true) { + $path = $_SERVER['REQUEST_URI']; + } elseif (is_string($requestUri)) { + $path = $requestUri; + } else { + $path = ''; + } + + return $this->getScheme() . '://' . $this->getHost() . $path; + } + + /** + * Returns host + * + * @return string host + */ + public function getHost() + { + return $this->host; + } + + /** + * Sets host + * + * @param string $host new host + * @return \Zend\View\Helper\ServerUrl fluent interface, returns self + */ + public function setHost($host) + { + $this->host = $host; + return $this; + } + + /** + * Returns scheme (typically http or https) + * + * @return string scheme (typically http or https) + */ + public function getScheme() + { + return $this->scheme; + } + + /** + * Sets scheme (typically http or https) + * + * @param string $scheme new scheme (typically http or https) + * @return \Zend\View\Helper\ServerUrl fluent interface, returns self + */ + public function setScheme($scheme) + { + $this->scheme = $scheme; + return $this; + } +} diff --git a/src/Helper/Url.php b/src/Helper/Url.php new file mode 100644 index 00000000..1ee7d40d --- /dev/null +++ b/src/Helper/Url.php @@ -0,0 +1,114 @@ +router = $router; + return $this; + } + + /** + * Set route match returned by the router. + * + * @param RouteMatch $routeMatch + * @return self + */ + public function setRouteMatch(RouteMatch $routeMatch) + { + $this->routeMatch = $routeMatch; + return $this; + } + + /** + * Generates an url given the name of a route. + * + * @see Zend\Mvc\Router\RouteInterface::assemble() + * @param string $name Name of the route + * @param array $params Parameters for the link + * @param array $options Options for the route + * @param boolean $reuseMatchedParams Whether to reuse matched parameters + * @return string Url For the link href attribute + * @throws Exception\RuntimeException If no RouteStackInterface was provided + * @throws Exception\RuntimeException If no RouteMatch was provided + * @throws Exception\RuntimeException If RouteMatch didn't contain a matched route name + */ + public function __invoke($name = null, array $params = array(), $options = array(), $reuseMatchedParams = false) + { + if (null === $this->router) { + throw new Exception\RuntimeException('No RouteStackInterface instance provided'); + } + + if (3 == func_num_args() && is_bool($options)) { + $reuseMatchedParams = $options; + $options = array(); + } + + if ($name === null) { + if ($this->routeMatch === null) { + throw new Exception\RuntimeException('No RouteMatch instance provided'); + } + + $name = $this->routeMatch->getMatchedRouteName(); + + if ($name === null) { + throw new Exception\RuntimeException('RouteMatch does not contain a matched route name'); + } + } + + if ($reuseMatchedParams && $this->routeMatch !== null) { + $routeMatchParams = $this->routeMatch->getParams(); + + if (isset($routeMatchParams[ModuleRouteListener::ORIGINAL_CONTROLLER])) { + $routeMatchParams['controller'] = $routeMatchParams[ModuleRouteListener::ORIGINAL_CONTROLLER]; + } + + $params = array_merge($routeMatchParams, $params); + } + + $options['name'] = $name; + + return $this->router->assemble($params, $options); + } +} diff --git a/src/Helper/ViewModel.php b/src/Helper/ViewModel.php new file mode 100644 index 00000000..a73cc28f --- /dev/null +++ b/src/Helper/ViewModel.php @@ -0,0 +1,96 @@ +root; + } + + /** + * Is a root view model composed? + * + * @return bool + */ + public function hasRoot() + { + return ($this->root instanceof Model); + } + + /** + * Get the current view model + * + * @return null|Model + */ + public function getCurrent() + { + return $this->current; + } + + /** + * Is a current view model composed? + * + * @return bool + */ + public function hasCurrent() + { + return ($this->current instanceof Model); + } + + /** + * Set the root view model + * + * @param Model $model + * @return ViewModel + */ + public function setRoot(Model $model) + { + $this->root = $model; + return $this; + } + + /** + * Set the current view model + * + * @param Model $model + * @return ViewModel + */ + public function setCurrent(Model $model) + { + $this->current = $model; + return $this; + } +} diff --git a/src/HelperPluginManager.php b/src/HelperPluginManager.php new file mode 100644 index 00000000..1c52945c --- /dev/null +++ b/src/HelperPluginManager.php @@ -0,0 +1,168 @@ + 'Zend\View\Helper\Doctype', // overridden by a factory in ViewHelperManagerFactory + 'basepath' => 'Zend\View\Helper\BasePath', + 'url' => 'Zend\View\Helper\Url', + 'cycle' => 'Zend\View\Helper\Cycle', + 'declarevars' => 'Zend\View\Helper\DeclareVars', + 'escapehtml' => 'Zend\View\Helper\EscapeHtml', + 'escapehtmlattr' => 'Zend\View\Helper\EscapeHtmlAttr', + 'escapejs' => 'Zend\View\Helper\EscapeJs', + 'escapecss' => 'Zend\View\Helper\EscapeCss', + 'escapeurl' => 'Zend\View\Helper\EscapeUrl', + 'gravatar' => 'Zend\View\Helper\Gravatar', + 'headlink' => 'Zend\View\Helper\HeadLink', + 'headmeta' => 'Zend\View\Helper\HeadMeta', + 'headscript' => 'Zend\View\Helper\HeadScript', + 'headstyle' => 'Zend\View\Helper\HeadStyle', + 'headtitle' => 'Zend\View\Helper\HeadTitle', + 'htmlflash' => 'Zend\View\Helper\HtmlFlash', + 'htmllist' => 'Zend\View\Helper\HtmlList', + 'htmlobject' => 'Zend\View\Helper\HtmlObject', + 'htmlpage' => 'Zend\View\Helper\HtmlPage', + 'htmlquicktime' => 'Zend\View\Helper\HtmlQuicktime', + 'inlinescript' => 'Zend\View\Helper\InlineScript', + 'json' => 'Zend\View\Helper\Json', + 'layout' => 'Zend\View\Helper\Layout', + 'paginationcontrol' => 'Zend\View\Helper\PaginationControl', + 'partialloop' => 'Zend\View\Helper\PartialLoop', + 'partial' => 'Zend\View\Helper\Partial', + 'placeholder' => 'Zend\View\Helper\Placeholder', + 'renderchildmodel' => 'Zend\View\Helper\RenderChildModel', + 'rendertoplaceholder' => 'Zend\View\Helper\RenderToPlaceholder', + 'serverurl' => 'Zend\View\Helper\ServerUrl', + 'viewmodel' => 'Zend\View\Helper\ViewModel', + ); + + /** + * @var Renderer\RendererInterface + */ + protected $renderer; + + /** + * Constructor + * + * After invoking parent constructor, add an initializer to inject the + * attached renderer and translator, if any, to the currently requested helper. + * + * @param null|ConfigInterface $configuration + */ + public function __construct(ConfigInterface $configuration = null) + { + parent::__construct($configuration); + $this->addInitializer(array($this, 'injectRenderer')) + ->addInitializer(array($this, 'injectTranslator')); + } + + /** + * Set renderer + * + * @param Renderer\RendererInterface $renderer + * @return HelperPluginManager + */ + public function setRenderer(Renderer\RendererInterface $renderer) + { + $this->renderer = $renderer; + return $this; + } + + /** + * Retrieve renderer instance + * + * @return null|Renderer\RendererInterface + */ + public function getRenderer() + { + return $this->renderer; + } + + /** + * Inject a helper instance with the registered renderer + * + * @param Helper\HelperInterface $helper + * @return void + */ + public function injectRenderer($helper) + { + $renderer = $this->getRenderer(); + if (null === $renderer) { + return; + } + $helper->setView($renderer); + } + + /** + * Inject a helper instance with the registered translator + * + * @param Helper\HelperInterface $helper + * @return void + */ + public function injectTranslator($helper) + { + if ($helper instanceof TranslatorAwareInterface) { + $locator = $this->getServiceLocator(); + if ($locator && $locator->has('translator')) { + $helper->setTranslator($locator->get('translator')); + } + } + } + + /** + * Validate the plugin + * + * Checks that the helper loaded is an instance of Helper\HelperInterface. + * + * @param mixed $plugin + * @return void + * @throws Exception\InvalidHelperException if invalid + */ + public function validatePlugin($plugin) + { + if ($plugin instanceof Helper\HelperInterface) { + // we're okay + return; + } + + throw new Exception\InvalidHelperException(sprintf( + 'Plugin of type %s is invalid; must implement %s\Helper\HelperInterface', + (is_object($plugin) ? get_class($plugin) : gettype($plugin)), + __NAMESPACE__ + )); + } +} diff --git a/src/Model/ConsoleModel.php b/src/Model/ConsoleModel.php new file mode 100644 index 00000000..e7af434a --- /dev/null +++ b/src/Model/ConsoleModel.php @@ -0,0 +1,93 @@ +options['errorLevel'] = $errorLevel; + } + + /** + * @return int + */ + public function getErrorLevel() + { + if (array_key_exists('errorLevel', $this->options)) { + return $this->options['errorLevel']; + } + } + + /** + * Set result text. + * + * @param string $text + * @return \Zend\View\Model\ConsoleModel + */ + public function setResult($text) + { + $this->setVariable(self::RESULT, $text); + return $this; + } + + /** + * Get result text. + * + * @return mixed + */ + public function getResult() + { + return $this->getVariable(self::RESULT); + } +} diff --git a/src/Model/FeedModel.php b/src/Model/FeedModel.php new file mode 100644 index 00000000..650ffc6b --- /dev/null +++ b/src/Model/FeedModel.php @@ -0,0 +1,94 @@ +feed instanceof Feed) { + return $this->feed; + } + + if (!$this->type) { + $options = $this->getOptions(); + if (isset($options['feed_type'])) { + $this->type = $options['feed_type']; + } + } + + $variables = $this->getVariables(); + $feed = FeedFactory::factory($variables); + $this->setFeed($feed); + + return $this->feed; + } + + /** + * Set the feed object + * + * @param Feed $feed + * @return FeedModel + */ + public function setFeed(Feed $feed) + { + $this->feed = $feed; + return $this; + } + + /** + * Get the feed type + * + * @return false|string + */ + public function getFeedType() + { + if ($this->type) { + return $this->type; + } + + $options = $this->getOptions(); + if (isset($options['feed_type'])) { + $this->type = $options['feed_type']; + } + return $this->type; + } +} diff --git a/src/Model/JsonModel.php b/src/Model/JsonModel.php new file mode 100644 index 00000000..9c6ade9d --- /dev/null +++ b/src/Model/JsonModel.php @@ -0,0 +1,76 @@ +jsonpCallback = $callback; + return $this; + } + + /** + * Serialize to JSON + * + * @return string + */ + public function serialize() + { + $variables = $this->getVariables(); + if ($variables instanceof Traversable) { + $variables = ArrayUtils::iteratorToArray($variables); + } + + if (!is_null($this->jsonpCallback)) { + return $this->jsonpCallback.'('.Json::encode($variables).');'; + } else { + return Json::encode($variables); + } + } +} diff --git a/src/Model/ModelInterface.php b/src/Model/ModelInterface.php new file mode 100644 index 00000000..99548c63 --- /dev/null +++ b/src/Model/ModelInterface.php @@ -0,0 +1,173 @@ +setVariables($variables, true); + + if (null !== $options) { + $this->setOptions($options); + } + } + + /** + * Property overloading: set variable value + * + * @param string $name + * @param mixed $value + * @return void + */ + public function __set($name, $value) + { + $this->setVariable($name, $value); + } + + /** + * Property overloading: get variable value + * + * @param string $name + * @return mixed + */ + public function __get($name) + { + if (!$this->__isset($name)) { + return null; + } + + $variables = $this->getVariables(); + return $variables[$name]; + } + + /** + * Property overloading: do we have the requested variable value? + * + * @param string $name + * @return bool + */ + public function __isset($name) + { + $variables = $this->getVariables(); + return isset($variables[$name]); + } + + /** + * Property overloading: unset the requested variable + * + * @param string $name + * @return void + */ + public function __unset($name) + { + if (!$this->__isset($name)) { + return null; + } + + unset($this->variables[$name]); + } + + /** + * Set a single option + * + * @param string $name + * @param mixed $value + * @return ViewModel + */ + public function setOption($name, $value) + { + $this->options[(string) $name] = $value; + return $this; + } + + /** + * Get a single option + * + * @param string $name The option to get. + * @param mixed|null $default (optional) A default value if the option is not yet set. + * @return mixed + */ + public function getOption($name, $default = null) + { + $name = (string)$name; + return array_key_exists($name, $this->options) ? $this->options[$name] : $default; + } + + /** + * Set renderer options/hints en masse + * + * @param array|\Traversable $options + * @throws \Zend\View\Exception\InvalidArgumentException + * @return ViewModel + */ + public function setOptions($options) + { + // Assumption is that lowest common denominator for renderer configuration + // is an array + if ($options instanceof Traversable) { + $options = ArrayUtils::iteratorToArray($options); + } + + if (!is_array($options)) { + throw new Exception\InvalidArgumentException(sprintf( + '%s: expects an array, or Traversable argument; received "%s"', + __METHOD__, + (is_object($options) ? get_class($options) : gettype($options)) + )); + } + + $this->options = $options; + return $this; + } + + /** + * Get renderer options/hints + * + * @return array + */ + public function getOptions() + { + return $this->options; + } + + /** + * Get a single view variable + * + * @param string $name + * @param mixed|null $default (optional) default value if the variable is not present. + * @return mixed + */ + public function getVariable($name, $default = null) + { + $name = (string)$name; + if (array_key_exists($name,$this->variables)) { + return $this->variables[$name]; + } else { + return $default; + } + } + + /** + * Set view variable + * + * @param string $name + * @param mixed $value + * @return ViewModel + */ + public function setVariable($name, $value) + { + $this->variables[(string) $name] = $value; + return $this; + } + + /** + * Set view variables en masse + * + * Can be an array or a Traversable + ArrayAccess object. + * + * @param array|ArrayAccess&Traversable $variables + * @param bool $overwrite Whether or not to overwrite the internal container with $variables + * @return ViewModel + */ + public function setVariables($variables, $overwrite = false) + { + if (!is_array($variables) && !$variables instanceof Traversable) { + throw new Exception\InvalidArgumentException(sprintf( + '%s: expects an array, or Traversable argument; received "%s"', + __METHOD__, + (is_object($variables) ? get_class($variables) : gettype($variables)) + )); + } + + if ($overwrite) { + if (is_object($variables) && !$variables instanceof ArrayAccess) { + $variables = ArrayUtils::iteratorToArray($variables); + } + + $this->variables = $variables; + return $this; + } + + foreach ($variables as $key => $value) { + $this->setVariable($key, $value); + } + + return $this; + } + + /** + * Get view variables + * + * @return array|ArrayAccess|Traversable + */ + public function getVariables() + { + return $this->variables; + } + + /** + * Set the template to be used by this model + * + * @param string $template + * @return ViewModel + */ + public function setTemplate($template) + { + $this->template = (string) $template; + return $this; + } + + /** + * Get the template to be used by this model + * + * @return string + */ + public function getTemplate() + { + return $this->template; + } + + /** + * Add a child model + * + * @param ModelInterface $child + * @param null|string $captureTo Optional; if specified, the "capture to" value to set on the child + * @param null|bool $append Optional; if specified, append to child with the same capture + * @return ViewModel + */ + public function addChild(ModelInterface $child, $captureTo = null, $append = null) + { + $this->children[] = $child; + if (null !== $captureTo) { + $child->setCaptureTo($captureTo); + } + if (null !== $captureTo) { + $child->setAppend($append); + } + + return $this; + } + + /** + * Return all children. + * + * Return specifies an array, but may be any iterable object. + * + * @return array + */ + public function getChildren() + { + return $this->children; + } + + /** + * Does the model have any children? + * + * @return bool + */ + public function hasChildren() + { + return (0 < count($this->children)); + } + + /** + * Set the name of the variable to capture this model to, if it is a child model + * + * @param string $capture + * @return ViewModel + */ + public function setCaptureTo($capture) + { + $this->captureTo = (string) $capture; + return $this; + } + + /** + * Get the name of the variable to which to capture this model + * + * @return string + */ + public function captureTo() + { + return $this->captureTo; + } + + /** + * Set flag indicating whether or not this is considered a terminal or standalone model + * + * @param bool $terminate + * @return ViewModel + */ + public function setTerminal($terminate) + { + $this->terminate = (bool) $terminate; + return $this; + } + + /** + * Is this considered a terminal or standalone model? + * + * @return bool + */ + public function terminate() + { + return $this->terminate; + } + + /** + * Set flag indicating whether or not append to child with the same capture + * + * @param bool $append + * @return ViewModel + */ + public function setAppend($append) + { + $this->append = (bool) $append; + return $this; + } + + /** + * Is this append to child with the same capture? + * + * @return bool + */ + public function isAppend() + { + return $this->append; + } + + /** + * Return count of children + * + * @return int + */ + public function count() + { + return count($this->children); + } + + /** + * Get iterator of children + * + * @return ArrayIterator + */ + public function getIterator() + { + return new ArrayIterator($this->children); + } +} diff --git a/src/Renderer/ConsoleRenderer.php b/src/Renderer/ConsoleRenderer.php new file mode 100644 index 00000000..d4070ef9 --- /dev/null +++ b/src/Renderer/ConsoleRenderer.php @@ -0,0 +1,155 @@ +init(); + } + + public function setResolver(ResolverInterface $resolver) + { + return $this; + } + + /** + * Return the template engine object + * + * Returns the object instance, as it is its own template engine + * + * @return PhpRenderer + */ + public function getEngine() + { + return $this; + } + + /** + * Allow custom object initialization when extending Zend_View_Abstract or + * Zend_View + * + * Triggered by {@link __construct() the constructor} as its final action. + * + * @return void + */ + public function init() + { + } + + /** + * Set filter chain + * + * @param FilterChain $filters + * @return ConsoleRenderer + */ + public function setFilterChain(FilterChain $filters) + { + $this->__filterChain = $filters; + return $this; + } + + /** + * Retrieve filter chain for post-filtering script content + * + * @return FilterChain + */ + public function getFilterChain() + { + if (null === $this->__filterChain) { + $this->setFilterChain(new FilterChain()); + } + return $this->__filterChain; + } + + /** + * Recursively processes all ViewModels and returns output. + * + * @param string|ModelInterface $model A ViewModel instance. + * @param null|array|\Traversable $values Values to use when rendering. If none + * provided, uses those in the composed + * variables container. + * @return string Console output. + */ + public function render($model, $values = null) + { + if (!$model instanceof ModelInterface) { + return ''; + } + + $result = ''; + $options = $model->getOptions(); + foreach ($options as $setting => $value) { + $method = 'set' . $setting; + if (method_exists($this, $method)) { + $this->$method($value); + } + unset($method, $setting, $value); + } + unset($options); + + $values = $model->getVariables(); + + if (isset($values['result'])) { + // filter and append the result + $result .= $this->getFilterChain()->filter($values['result']); + } + + if ($model->hasChildren()) { + // recursively render all children + foreach ($model->getChildren() as $child) { + $result .= $this->render($child, $values); + } + } + + return $result; + } + + /** + * @see Zend\View\Renderer\TreeRendererInterface + * @return bool + */ + public function canRenderTrees() + { + return true; + } + +} diff --git a/src/Renderer/FeedRenderer.php b/src/Renderer/FeedRenderer.php new file mode 100644 index 00000000..f4f36256 --- /dev/null +++ b/src/Renderer/FeedRenderer.php @@ -0,0 +1,141 @@ +resolver = $resolver; + } + + /** + * Renders values as JSON + * + * @todo Determine what use case exists for accepting only $nameOrModel + * @param string|Model $name The script/resource process, or a view model + * @param null|array|\ArrayAccess Values to use during rendering + * @return string The script output. + */ + public function render($nameOrModel, $values = null) + { + if ($nameOrModel instanceof Model) { + // Use case 1: View Model provided + // Non-FeedModel: cast to FeedModel + if (!$nameOrModel instanceof FeedModel) { + $vars = $nameOrModel->getVariables(); + $options = $nameOrModel->getOptions(); + $type = $this->getFeedType(); + if (isset($options['feed_type'])) { + $type = $options['feed_type']; + } else { + $this->setFeedType($type); + } + $nameOrModel = new FeedModel($vars, array('feed_type' => $type)); + } + } elseif (is_string($nameOrModel)) { + // Use case 2: string $nameOrModel + array|Traversable|Feed $values + $nameOrModel = new FeedModel($values, (array) $nameOrModel); + } else { + // Use case 3: failure + throw new Exception\InvalidArgumentException(sprintf( + '%s expects a ViewModel or a string feed type as the first argument; received "%s"', + __METHOD__, + (is_object($nameOrModel) ? get_class($nameOrModel) : gettype($nameOrModel)) + )); + } + + // Get feed and type + $feed = $nameOrModel->getFeed(); + $type = $nameOrModel->getFeedType(); + if (!$type) { + $type = $this->getFeedType(); + } else { + $this->setFeedType($type); + } + + // Render feed + return $feed->export($type); + } + + /** + * Set feed type ('rss' or 'atom') + * + * @param string $feedType + * @return FeedRenderer + */ + public function setFeedType($feedType) + { + $feedType = strtolower($feedType); + if (!in_array($feedType, array('rss', 'atom'))) { + throw new Exception\InvalidArgumentException(sprintf( + '%s expects a string of either "rss" or "atom"', + __METHOD__ + )); + } + + $this->feedType = $feedType; + return $this; + } + + /** + * Get feed type + * + * @return string + */ + public function getFeedType() + { + return $this->feedType; + } +} diff --git a/src/Renderer/JsonRenderer.php b/src/Renderer/JsonRenderer.php new file mode 100644 index 00000000..97a91f54 --- /dev/null +++ b/src/Renderer/JsonRenderer.php @@ -0,0 +1,223 @@ +resolver = $resolver; + } + + /** + * Set flag indicating whether or not to merge unnamed children + * + * @param bool $mergeUnnamedChildren + * @return JsonRenderer + */ + public function setMergeUnnamedChildren($mergeUnnamedChildren) + { + $this->mergeUnnamedChildren = (bool) $mergeUnnamedChildren; + return $this; + } + + /** + * Set the JSONP callback function name + * + * @param string $callback + * @return JsonpModel + */ + public function setJsonpCallback($callback) + { + $callback = (string) $callback; + if (!empty($callback)) { + $this->jsonpCallback = $callback; + } + return $this; + } + + /** + * Returns whether or not the jsonpCallback has been set + * + * @return bool + */ + public function hasJsonpCallback() + { + return !is_null($this->jsonpCallback); + } + + /** + * Should we merge unnamed children? + * + * @return bool + */ + public function mergeUnnamedChildren() + { + return $this->mergeUnnamedChildren; + } + + /** + * Renders values as JSON + * + * @todo Determine what use case exists for accepting both $nameOrModel and $values + * @param string|Model $name The script/resource process, or a view model + * @param null|array|\ArrayAccess Values to use during rendering + * @return string The script output. + */ + public function render($nameOrModel, $values = null) + { + // use case 1: View Models + // Serialize variables in view model + if ($nameOrModel instanceof Model) { + if ($nameOrModel instanceof JsonModel) { + $values = $nameOrModel->serialize(); + } else { + $values = $this->recurseModel($nameOrModel); + $values = Json::encode($values); + } + + if ($this->hasJsonpCallback()) { + $values = $this->jsonpCallback.'('.$values.');'; + } + return $values; + } + + // use case 2: $nameOrModel is populated, $values is not + // Serialize $nameOrModel + if (null === $values) { + if (!is_object($nameOrModel) || $nameOrModel instanceof JsonSerializable) { + $return = Json::encode($nameOrModel); + } elseif ($nameOrModel instanceof Traversable) { + $nameOrModel = ArrayUtils::iteratorToArray($nameOrModel); + $return = Json::encode($nameOrModel); + } else { + $return = Json::encode(get_object_vars($nameOrModel)); + } + + if ($this->hasJsonpCallback()) { + $return = $this->jsonpCallback.'('.$return.');'; + } + return $return; + } + + // use case 3: Both $nameOrModel and $values are populated + throw new Exception\DomainException(sprintf( + '%s: Do not know how to handle operation when both $nameOrModel and $values are populated', + __METHOD__ + )); + } + + /** + * Can this renderer render trees of view models? + * + * Yes. + * + * @return true + */ + public function canRenderTrees() + { + return true; + } + + /** + * Retrieve values from a model and recurse its children to build a data structure + * + * @param Model $model + * @return array + */ + protected function recurseModel(Model $model) + { + $values = $model->getVariables(); + if ($values instanceof Traversable) { + $values = ArrayUtils::iteratorToArray($values); + } + + if (!$model->hasChildren()) { + return $values; + } + + $mergeChildren = $this->mergeUnnamedChildren(); + foreach ($model as $child) { + $captureTo = $child->captureTo(); + if (!$captureTo && !$mergeChildren) { + // We don't want to do anything with this child + continue; + } + + $childValues = $this->recurseModel($child); + if ($captureTo) { + // Capturing to a specific key + //TODO please complete if append is true. must change old value to array and append to array? + $values[$captureTo] = $childValues; + } elseif ($mergeChildren) { + // Merging values with parent + $values = array_replace_recursive($values, $childValues); + } + } + return $values; + } +} diff --git a/src/Renderer/PhpRenderer.php b/src/Renderer/PhpRenderer.php new file mode 100644 index 00000000..f861f4db --- /dev/null +++ b/src/Renderer/PhpRenderer.php @@ -0,0 +1,516 @@ +init(); + } + + /** + * Return the template engine object + * + * Returns the object instance, as it is its own template engine + * + * @return PhpRenderer + */ + public function getEngine() + { + return $this; + } + + /** + * Allow custom object initialization when extending Zend_View_Abstract or + * Zend_View + * + * Triggered by {@link __construct() the constructor} as its final action. + * + * @return void + */ + public function init() + { + } + + /** + * Set script resolver + * + * @param Resolver $resolver + * @return PhpRenderer + * @throws Exception\InvalidArgumentException + */ + public function setResolver(Resolver $resolver) + { + $this->__templateResolver = $resolver; + return $this; + } + + /** + * Retrieve template name or template resolver + * + * @param null|string $name + * @return string|Resolver + */ + public function resolver($name = null) + { + if (null === $this->__templateResolver) { + $this->setResolver(new TemplatePathStack()); + } + + if (null !== $name) { + return $this->__templateResolver->resolve($name, $this); + } + + return $this->__templateResolver; + } + + /** + * Set variable storage + * + * Expects either an array, or an object implementing ArrayAccess. + * + * @param array|ArrayAccess $variables + * @return PhpRenderer + * @throws Exception\InvalidArgumentException + */ + public function setVars($variables) + { + if (!is_array($variables) && !$variables instanceof ArrayAccess) { + throw new Exception\InvalidArgumentException(sprintf( + 'Expected array or ArrayAccess object; received "%s"', + (is_object($variables) ? get_class($variables) : gettype($variables)) + )); + } + + // Enforce a Variables container + if (!$variables instanceof Variables) { + $variablesAsArray = array(); + foreach ($variables as $key => $value) { + $variablesAsArray[$key] = $value; + } + $variables = new Variables($variablesAsArray); + } + + $this->__vars = $variables; + return $this; + } + + /** + * Get a single variable, or all variables + * + * @param mixed $key + * @return mixed + */ + public function vars($key = null) + { + if (null === $this->__vars) { + $this->setVars(new Variables()); + } + + if (null === $key) { + return $this->__vars; + } + return $this->__vars[$key]; + } + + /** + * Get a single variable + * + * @param mixed $key + * @return mixed + */ + public function get($key) + { + if (null === $this->__vars) { + $this->setVars(new Variables()); + } + + return $this->__vars[$key]; + } + + /** + * Overloading: proxy to Variables container + * + * @param string $name + * @return mixed + */ + public function __get($name) + { + $vars = $this->vars(); + return $vars[$name]; + } + + /** + * Overloading: proxy to Variables container + * + * @param string $name + * @param mixed $value + * @return void + */ + public function __set($name, $value) + { + $vars = $this->vars(); + $vars[$name] = $value; + } + + /** + * Overloading: proxy to Variables container + * + * @param string $name + * @return bool + */ + public function __isset($name) + { + $vars = $this->vars(); + return isset($vars[$name]); + } + + /** + * Overloading: proxy to Variables container + * + * @param string $name + * @return void + */ + public function __unset($name) + { + $vars = $this->vars(); + if (!isset($vars[$name])) { + return; + } + unset($vars[$name]); + } + + /** + * Set helper plugin manager instance + * + * @param string|HelperPluginManager $helpers + * @return PhpRenderer + * @throws Exception\InvalidArgumentException + */ + public function setHelperPluginManager($helpers) + { + if (is_string($helpers)) { + if (!class_exists($helpers)) { + throw new Exception\InvalidArgumentException(sprintf( + 'Invalid helper helpers class provided (%s)', + $helpers + )); + } + $helpers = new $helpers(); + } + if (!$helpers instanceof HelperPluginManager) { + throw new Exception\InvalidArgumentException(sprintf( + 'Helper helpers must extend Zend\View\HelperPluginManager; got type "%s" instead', + (is_object($helpers) ? get_class($helpers) : gettype($helpers)) + )); + } + $helpers->setRenderer($this); + $this->__helpers = $helpers; + + return $this; + } + + /** + * Get helper plugin manager instance + * + * @return HelperPluginManager + */ + public function getHelperPluginManager() + { + if (null === $this->__helpers) { + $this->setHelperPluginManager(new HelperPluginManager()); + } + return $this->__helpers; + } + + /** + * Get plugin instance + * + * @param string $plugin Name of plugin to return + * @param null|array $options Options to pass to plugin constructor (if not already instantiated) + * @return Helper + */ + public function plugin($name, array $options = null) + { + return $this->getHelperPluginManager()->get($name, $options); + } + + /** + * Overloading: proxy to helpers + * + * Proxies to the attached plugin manager to retrieve, return, and potentially + * execute helpers. + * + * * If the helper does not define __invoke, it will be returned + * * If the helper does define __invoke, it will be called as a functor + * + * @param string $method + * @param array $argv + * @return mixed + */ + public function __call($method, $argv) + { + $helper = $this->plugin($method); + if (is_callable($helper)) { + return call_user_func_array($helper, $argv); + } + return $helper; + } + + /** + * Set filter chain + * + * @param FilterChain $filters + * @return PhpRenderer + */ + public function setFilterChain(FilterChain $filters) + { + $this->__filterChain = $filters; + return $this; + } + + /** + * Retrieve filter chain for post-filtering script content + * + * @return FilterChain + */ + public function getFilterChain() + { + if (null === $this->__filterChain) { + $this->setFilterChain(new FilterChain()); + } + return $this->__filterChain; + } + + /** + * Processes a view script and returns the output. + * + * @param string|Model $nameOrModel Either the template to use, or a + * ViewModel. The ViewModel must have the + * template as an option in order to be + * valid. + * @param null|array|Traversable Values to use when rendering. If none + * provided, uses those in the composed + * variables container. + * @return string The script output. + * @throws Exception\DomainException if a ViewModel is passed, but does not + * contain a template option. + * @throws Exception\InvalidArgumentException if the values passed are not + * an array or ArrayAccess object + */ + public function render($nameOrModel, $values = null) + { + if ($nameOrModel instanceof Model) { + $model = $nameOrModel; + $nameOrModel = $model->getTemplate(); + if (empty($nameOrModel)) { + throw new Exception\DomainException(sprintf( + '%s: received View Model argument, but template is empty', + __METHOD__ + )); + } + $options = $model->getOptions(); + foreach ($options as $setting => $value) { + $method = 'set' . $setting; + if (method_exists($this, $method)) { + $this->$method($value); + } + unset($method, $setting, $value); + } + unset($options); + + // Give view model awareness via ViewModel helper + $helper = $this->plugin('view_model'); + $helper->setCurrent($model); + + $values = $model->getVariables(); + unset($model); + } + + // find the script file name using the parent private method + $this->addTemplate($nameOrModel); + unset($nameOrModel); // remove $name from local scope + + $this->__varsCache[] = $this->vars(); + + if (null !== $values) { + $this->setVars($values); + } + unset($values); + + // extract all assigned vars (pre-escaped), but not 'this'. + // assigns to a double-underscored variable, to prevent naming collisions + $__vars = $this->vars()->getArrayCopy(); + if (array_key_exists('this', $__vars)) { + unset($__vars['this']); + } + extract($__vars); + unset($__vars); // remove $__vars from local scope + + while ($this->__template = array_pop($this->__templates)) { + $this->__file = $this->resolver($this->__template); + if (!$this->__file) { + throw new Exception\RuntimeException(sprintf( + '%s: Unable to render template "%s"; resolver could not resolve to a file', + __METHOD__, + $this->__template + )); + } + ob_start(); + include $this->__file; + $this->__content = ob_get_clean(); + } + + $this->setVars(array_pop($this->__varsCache)); + + return $this->getFilterChain()->filter($this->__content); // filter output + } + + /** + * Set flag indicating whether or not we should render trees of view models + * + * If set to true, the View instance will not attempt to render children + * separately, but instead pass the root view model directly to the PhpRenderer. + * It is then up to the developer to render the children from within the + * view script. + * + * @param bool $renderTrees + * @return PhpRenderer + */ + public function setCanRenderTrees($renderTrees) + { + $this->__renderTrees = (bool) $renderTrees; + return $this; + } + + /** + * Can we render trees, or are we configured to do so? + * + * @return bool + */ + public function canRenderTrees() + { + return $this->__renderTrees; + } + + /** + * Add a template to the stack + * + * @param string $template + * @return PhpRenderer + */ + public function addTemplate($template) + { + $this->__templates[] = $template; + return $this; + } + + /** + * Make sure View variables are cloned when the view is cloned. + * + * @return PhpRenderer + */ + public function __clone() + { + $this->__vars = clone $this->vars(); + } + +} diff --git a/src/Renderer/RendererInterface.php b/src/Renderer/RendererInterface.php new file mode 100644 index 00000000..0cb38ec8 --- /dev/null +++ b/src/Renderer/RendererInterface.php @@ -0,0 +1,51 @@ +queue = new PriorityQueue(); + } + + /** + * Return count of attached resolvers + * + * @return void + */ + public function count() + { + return $this->queue->count(); + } + + /** + * IteratorAggregate: return internal iterator + * + * @return Traversable + */ + public function getIterator() + { + return $this->queue; + } + + /** + * Attach a resolver + * + * @param Resolver $resolver + * @param int $priority + * @return AggregateResolver + */ + public function attach(Resolver $resolver, $priority = 1) + { + $this->queue->insert($resolver, $priority); + return $this; + } + + /** + * Resolve a template/pattern name to a resource the renderer can consume + * + * @param string $name + * @param null|Renderer $renderer + * @return false|string + */ + public function resolve($name, Renderer $renderer = null) + { + $this->lastLookupFailure = false; + $this->lastSuccessfulResolver = null; + + if (0 === count($this->queue)) { + $this->lastLookupFailure = static::FAILURE_NO_RESOLVERS; + return false; + } + + foreach ($this->queue as $resolver) { + $resource = $resolver->resolve($name, $renderer); + if (!$resource) { + // No resource found; try next resolver + continue; + } + + // Resource found; return it + $this->lastSuccessfulResolver = $resolver; + return $resource; + } + + $this->lastLookupFailure = static::FAILURE_NOT_FOUND; + return false; + } + + /** + * Return the last successful resolver, if any + * + * @return Resolver + */ + public function getLastSuccessfulResolver() + { + return $this->lastSuccessfulResolver; + } + + /** + * Get last lookup failure + * + * @return false|string + */ + public function getLastLookupFailure() + { + return $this->lastLookupFailure; + } +} diff --git a/src/Resolver/ResolverInterface.php b/src/Resolver/ResolverInterface.php new file mode 100644 index 00000000..2759be09 --- /dev/null +++ b/src/Resolver/ResolverInterface.php @@ -0,0 +1,30 @@ +setMap($map); + } + + /** + * IteratorAggregate: return internal iterator + * + * @return Traversable + */ + public function getIterator() + { + return new ArrayIterator($this->map); + } + + /** + * Set (overwrite) template map + * + * Maps should be arrays or Traversable objects with name => path pairs + * + * @param array|Traversable $map + * @return TemplateMapResolver + */ + public function setMap($map) + { + if (!is_array($map) && !$map instanceof Traversable) { + throw new Exception\InvalidArgumentException(sprintf( + '%s: expects an array or Traversable, received "%s"', + __METHOD__, + (is_object($map) ? get_class($map) : gettype($map)) + )); + } + + if ($map instanceof Traversable) { + $map = ArrayUtils::iteratorToArray($map); + } + + $this->map = $map; + return $this; + } + + /** + * Add an entry to the map + * + * @param string|array|Traversable $nameOrMap + * @param null|string $path + * @return TemplateResolver + */ + public function add($nameOrMap, $path = null) + { + if (is_array($nameOrMap) || $nameOrMap instanceof Traversable) { + $this->merge($nameOrMap); + return $this; + } + + if (!is_string($nameOrMap)) { + throw new Exception\InvalidArgumentException(sprintf( + '%s: expects a string, array, or Traversable for the first argument; received "%s"', + __METHOD__, + (is_object($map) ? get_class($map) : gettype($map)) + )); + } + + if (empty($path)) { + if (isset($this->map[$nameOrMap])) { + unset($this->map[$nameOrMap]); + } + return $this; + } + + $this->map[$nameOrMap] = $path; + return $this; + } + + /** + * Merge internal map with provided map + * + * @param array|Traversable $map + * @return TemplateMapResolver + */ + public function merge($map) + { + if (!is_array($map) && !$map instanceof Traversable) { + throw new Exception\InvalidArgumentException(sprintf( + '%s: expects an array or Traversable, received "%s"', + __METHOD__, + (is_object($map) ? get_class($map) : gettype($map)) + )); + } + + if ($map instanceof Traversable) { + $map = ArrayUtils::iteratorToArray($map); + } + + $this->map = array_replace_recursive($this->map, $map); + return $this; + } + + /** + * Does the resolver contain an entry for the given name? + * + * @param string $name + * @return bool + */ + public function has($name) + { + return array_key_exists($name, $this->map); + } + + /** + * Retrieve a template path by name + * + * @param string $name + * @return false|string + * @throws Exception\DomainException if no entry exists + */ + public function get($name) + { + if (!$this->has($name)) { + return false; + } + return $this->map[$name]; + } + + /** + * Retrieve the template map + * + * @return array + */ + public function getMap() + { + return $this->map; + } + + /** + * Resolve a template/pattern name to a resource the renderer can consume + * + * @param string $name + * @param null|Renderer $renderer + * @return string + */ + public function resolve($name, Renderer $renderer = null) + { + return $this->get($name); + } +} diff --git a/src/Resolver/TemplatePathStack.php b/src/Resolver/TemplatePathStack.php new file mode 100644 index 00000000..49667a10 --- /dev/null +++ b/src/Resolver/TemplatePathStack.php @@ -0,0 +1,339 @@ +useViewStream = (bool) ini_get('short_open_tag'); + if ($this->useViewStream) { + if (!in_array('zend.view', stream_get_wrappers())) { + stream_wrapper_register('zend.view', 'Zend\View\Stream'); + } + } + + $this->paths = new SplStack; + if (null !== $options) { + $this->setOptions($options); + } + } + + /** + * Configure object + * + * @param array|\Traversable $options + * @return void + * @throws Exception\InvalidArgumentException + */ + public function setOptions($options) + { + if (!is_array($options) && !$options instanceof \Traversable) { + throw new Exception\InvalidArgumentException(sprintf( + 'Expected array or Traversable object; received "%s"', + (is_object($options) ? get_class($options) : gettype($options)) + )); + } + + foreach ($options as $key => $value) { + switch (strtolower($key)) { + case 'lfi_protection': + $this->setLfiProtection($value); + break; + case 'script_paths': + $this->addPaths($value); + break; + case 'use_stream_wrapper': + $this->setUseStreamWrapper($value); + break; + default: + break; + } + } + } + + /** + * Set default file suffix + * + * @param string $defaultSuffix + * @return TemplatePathStack + */ + public function setDefaultSuffix($defaultSuffix) + { + $this->defaultSuffix = (string) $defaultSuffix; + $this->defaultSuffix = ltrim($this->defaultSuffix, '.'); + return $this; + } + + /** + * Get default file suffix + * + * @return string + */ + public function getDefaultSuffix() + { + return $this->defaultSuffix; + } + + /** + * Add many paths to the stack at once + * + * @param array $paths + * @return TemplatePathStack + */ + public function addPaths(array $paths) + { + foreach ($paths as $path) { + $this->addPath($path); + } + return $this; + } + + /** + * Rest the path stack to the paths provided + * + * @param SplStack|array $paths + * @return TemplatePathStack + * @throws Exception\InvalidArgumentException + */ + public function setPaths($paths) + { + if ($paths instanceof SplStack) { + $this->paths = $paths; + } elseif (is_array($paths)) { + $this->clearPaths(); + $this->addPaths($paths); + } else { + throw new Exception\InvalidArgumentException( + "Invalid argument provided for \$paths, expecting either an array or SplStack object" + ); + } + + return $this; + } + + /** + * Normalize a path for insertion in the stack + * + * @param string $path + * @return string + */ + public static function normalizePath($path) + { + $path = rtrim($path, '/'); + $path = rtrim($path, '\\'); + $path .= DIRECTORY_SEPARATOR; + return $path; + } + + /** + * Add a single path to the stack + * + * @param string $path + * @return TemplatePathStack + * @throws Exception\InvalidArgumentException + */ + public function addPath($path) + { + if (!is_string($path)) { + throw new Exception\InvalidArgumentException(sprintf( + 'Invalid path provided; must be a string, received %s', + gettype($path) + )); + } + $this->paths[] = static::normalizePath($path); + return $this; + } + + /** + * Clear all paths + * + * @return void + */ + public function clearPaths() + { + $this->paths = new SplStack; + } + + /** + * Returns stack of paths + * + * @return SplStack + */ + public function getPaths() + { + return $this->paths; + } + + /** + * Set LFI protection flag + * + * @param bool $flag + * @return TemplatePathStack + */ + public function setLfiProtection($flag) + { + $this->lfiProtectionOn = (bool) $flag; + return $this; + } + + /** + * Return status of LFI protection flag + * + * @return bool + */ + public function isLfiProtectionOn() + { + return $this->lfiProtectionOn; + } + + /** + * Set flag indicating if stream wrapper should be used if short_open_tag is off + * + * @param bool $flag + * @return TemplatePathStack + */ + public function setUseStreamWrapper($flag) + { + $this->useStreamWrapper = (bool) $flag; + return $this; + } + + /** + * Should the stream wrapper be used if short_open_tag is off? + * + * Returns true if the use_stream_wrapper flag is set, and if short_open_tag + * is disabled. + * + * @return bool + */ + public function useStreamWrapper() + { + return ($this->useViewStream && $this->useStreamWrapper); + } + + /** + * Retrieve the filesystem path to a view script + * + * @param string $name + * @param null|Renderer $renderer + * @return string + * @throws Exception\RuntimeException + */ + public function resolve($name, Renderer $renderer = null) + { + $this->lastLookupFailure = false; + + if ($this->isLfiProtectionOn() && preg_match('#\.\.[\\\/]#', $name)) { + throw new Exception\DomainException( + 'Requested scripts may not include parent directory traversal ("../", "..\\" notation)' + ); + } + + if (!count($this->paths)) { + $this->lastLookupFailure = static::FAILURE_NO_PATHS; + return false; + } + + // Ensure we have the expected file extension + $defaultSuffix = $this->getDefaultSuffix(); + if (pathinfo($name, PATHINFO_EXTENSION) != $defaultSuffix) {; + $name .= '.' . $defaultSuffix; + } + + foreach ($this->paths as $path) { + $file = new SplFileInfo($path . $name); + if ($file->isReadable()) { + // Found! Return it. + if (($filePath = $file->getRealPath()) === false && substr($path, 0, 7) === 'phar://') { + // Do not try to expand phar paths (realpath + phars == fail) + $filePath = $path . $name; + if (!file_exists($filePath)) { + break; + } + } + if ($this->useStreamWrapper()) { + // If using a stream wrapper, prepend the spec to the path + $filePath = 'zend.view://' . $filePath; + } + return $filePath; + } + } + + $this->lastLookupFailure = static::FAILURE_NOT_FOUND; + return false; + } + + /** + * Get the last lookup failure message, if any + * + * @return false|string + */ + public function getLastLookupFailure() + { + return $this->lastLookupFailure; + } +} diff --git a/src/Strategy/FeedStrategy.php b/src/Strategy/FeedStrategy.php new file mode 100644 index 00000000..27a79dc7 --- /dev/null +++ b/src/Strategy/FeedStrategy.php @@ -0,0 +1,168 @@ +renderer = $renderer; + } + + /** + * Attach the aggregate to the specified event manager + * + * @param EventManagerInterface $events + * @param int $priority + * @return void + */ + public function attach(EventManagerInterface $events, $priority = 1) + { + $this->listeners[] = $events->attach(ViewEvent::EVENT_RENDERER, array($this, 'selectRenderer'), $priority); + $this->listeners[] = $events->attach(ViewEvent::EVENT_RESPONSE, array($this, 'injectResponse'), $priority); + } + + /** + * Detach aggregate listeners from the specified event manager + * + * @param EventManagerInterface $events + * @return void + */ + public function detach(EventManagerInterface $events) + { + foreach ($this->listeners as $index => $listener) { + if ($events->detach($listener)) { + unset($this->listeners[$index]); + } + } + } + + /** + * Detect if we should use the FeedRenderer based on model type and/or + * Accept header + * + * @param ViewEvent $e + * @return null|FeedRenderer + */ + public function selectRenderer(ViewEvent $e) + { + $model = $e->getModel(); + + if ($model instanceof Model\FeedModel) { + // FeedModel found + return $this->renderer; + } + + $request = $e->getRequest(); + if (!$request instanceof HttpRequest) { + // Not an HTTP request; cannot autodetermine + return; + } + + $headers = $request->getHeaders(); + if (!$headers->has('accept')) { + return; + } + + $accept = $headers->get('accept'); + if (($match = $accept->match('application/rss+xml, application/atom+xml')) == false) { + return; + } + + if ($match->getTypeString() == 'application/rss+xml') { + $this->renderer->setFeedType('rss'); + return $this->renderer; + } + + if ($match->getTypeString() == 'application/atom+xml') { + $this->renderer->setFeedType('atom'); + return $this->renderer; + } + + } + + /** + * Inject the response with the feed payload and appropriate Content-Type header + * + * @param ViewEvent $e + * @return void + */ + public function injectResponse(ViewEvent $e) + { + $renderer = $e->getRenderer(); + if ($renderer !== $this->renderer) { + // Discovered renderer is not ours; do nothing + return; + } + + $result = $e->getResult(); + if (!is_string($result) && !$result instanceof Feed) { + // We don't have a string, and thus, no feed + return; + } + + // If the result is a feed, export it + if ($result instanceof Feed) { + $result = $result->export($renderer->getFeedType()); + } + + // Get the content-type header based on feed type + $feedType = $renderer->getFeedType(); + $feedType = ('rss' == $feedType) + ? 'application/rss+xml' + : 'application/atom+xml'; + + $model = $e->getModel(); + $charset = ''; + + if ($model instanceof Model\FeedModel) { + + $feed = $model->getFeed(); + + $charset = '; charset=' . $feed->getEncoding() . ';'; + } + + // Populate response + $response = $e->getResponse(); + $response->setContent($result); + $headers = $response->getHeaders(); + $headers->addHeaderLine('content-type', $feedType . $charset); + } +} diff --git a/src/Strategy/JsonStrategy.php b/src/Strategy/JsonStrategy.php new file mode 100644 index 00000000..d4536e8d --- /dev/null +++ b/src/Strategy/JsonStrategy.php @@ -0,0 +1,153 @@ +renderer = $renderer; + } + + /** + * Attach the aggregate to the specified event manager + * + * @param EventManagerInterface $events + * @param int $priority + * @return void + */ + public function attach(EventManagerInterface $events, $priority = 1) + { + $this->listeners[] = $events->attach(ViewEvent::EVENT_RENDERER, array($this, 'selectRenderer'), $priority); + $this->listeners[] = $events->attach(ViewEvent::EVENT_RESPONSE, array($this, 'injectResponse'), $priority); + } + + /** + * Detach aggregate listeners from the specified event manager + * + * @param EventManagerInterface $events + * @return void + */ + public function detach(EventManagerInterface $events) + { + foreach ($this->listeners as $index => $listener) { + if ($events->detach($listener)) { + unset($this->listeners[$index]); + } + } + } + + /** + * Detect if we should use the JsonRenderer based on model type and/or + * Accept header + * + * @param ViewEvent $e + * @return null|JsonRenderer + */ + public function selectRenderer(ViewEvent $e) + { + $model = $e->getModel(); + + if ($model instanceof Model\JsonModel) { + // JsonModel found + return $this->renderer; + } + + $request = $e->getRequest(); + if (!$request instanceof HttpRequest) { + // Not an HTTP request; cannot autodetermine + return; + } + + $headers = $request->getHeaders(); + if (!$headers->has('accept')) { + return; + } + + + $accept = $headers->get('Accept'); + if (($match = $accept->match('application/json, application/javascript')) == false) { + return; + } + + if ($match->getTypeString() == 'application/json') { + // application/json Accept header found + return $this->renderer; + } + + if ($match->getTypeString() == 'application/javascript') { + // application/javascript Accept header found + if (false != ($callback = $request->getQuery()->get('callback'))) { + $this->renderer->setJsonpCallback($callback); + } + return $this->renderer; + } + } + + /** + * Inject the response with the JSON payload and appropriate Content-Type header + * + * @param ViewEvent $e + * @return void + */ + public function injectResponse(ViewEvent $e) + { + $renderer = $e->getRenderer(); + if ($renderer !== $this->renderer) { + // Discovered renderer is not ours; do nothing + return; + } + + $result = $e->getResult(); + if (!is_string($result)) { + // We don't have a string, and thus, no JSON + return; + } + + // Populate response + $response = $e->getResponse(); + $response->setContent($result); + $headers = $response->getHeaders(); + if ($this->renderer->hasJsonpCallback()) { + $headers->addHeaderLine('content-type', 'application/javascript'); + } else { + $headers->addHeaderLine('content-type', 'application/json'); + } + } +} diff --git a/src/Strategy/PhpRendererStrategy.php b/src/Strategy/PhpRendererStrategy.php new file mode 100644 index 00000000..2839af7c --- /dev/null +++ b/src/Strategy/PhpRendererStrategy.php @@ -0,0 +1,160 @@ +renderer = $renderer; + } + + /** + * Retrieve the composed renderer + * + * @return PhpRenderer + */ + public function getRenderer() + { + return $this->renderer; + } + + /** + * Set list of possible content placeholders + * + * @param array contentPlaceholders + * @return PhpRendererStrategy + */ + public function setContentPlaceholders(array $contentPlaceholders) + { + $this->contentPlaceholders = $contentPlaceholders; + return $this; + } + + /** + * Get list of possible content placeholders + * + * @return array + */ + public function getContentPlaceholders() + { + return $this->contentPlaceholders; + } + + /** + * Attach the aggregate to the specified event manager + * + * @param EventManagerInterface $events + * @param int $priority + * @return void + */ + public function attach(EventManagerInterface $events, $priority = 1) + { + $this->listeners[] = $events->attach(ViewEvent::EVENT_RENDERER, array($this, 'selectRenderer'), $priority); + $this->listeners[] = $events->attach(ViewEvent::EVENT_RESPONSE, array($this, 'injectResponse'), $priority); + } + + /** + * Detach aggregate listeners from the specified event manager + * + * @param EventManagerInterface $events + * @return void + */ + public function detach(EventManagerInterface $events) + { + foreach ($this->listeners as $index => $listener) { + if ($events->detach($listener)) { + unset($this->listeners[$index]); + } + } + } + + /** + * Select the PhpRenderer; typically, this will be registered last or at + * low priority. + * + * @param ViewEvent $e + * @return PhpRenderer + */ + public function selectRenderer(ViewEvent $e) + { + return $this->renderer; + } + + /** + * Populate the response object from the View + * + * Populates the content of the response object from the view rendering + * results. + * + * @param ViewEvent $e + * @return void + */ + public function injectResponse(ViewEvent $e) + { + $renderer = $e->getRenderer(); + if ($renderer !== $this->renderer) { + return; + } + + $result = $e->getResult(); + $response = $e->getResponse(); + + // Set content + // If content is empty, check common placeholders to determine if they are + // populated, and set the content from them. + if (empty($result)) { + $placeholders = $renderer->plugin('placeholder'); + $registry = $placeholders->getRegistry(); + foreach ($this->contentPlaceholders as $placeholder) { + if ($registry->containerExists($placeholder)) { + $result = (string) $registry->getContainer($placeholder); + break; + } + } + } + $response->setContent($result); + } +} diff --git a/src/Stream.php b/src/Stream.php new file mode 100644 index 00000000..3241c662 --- /dev/null +++ b/src/Stream.php @@ -0,0 +1,172 @@ +data = file_get_contents($path); + + /** + * If reading the file failed, update our local stat store + * to reflect the real stat of the file, then return on failure + */ + if ($this->data === false) { + $this->stat = stat($path); + return false; + } + + /** + * Convert to long-form and to + * + */ + $this->data = preg_replace('/\<\?\=/', "data); + $this->data = preg_replace('/<\?(?!xml|php)/s', 'data); + + /** + * file_get_contents() won't update PHP's stat cache, so we grab a stat + * of the file to prevent additional reads should the script be + * requested again, which will make include() happy. + */ + $this->stat = stat($path); + + return true; + } + + /** + * Included so that __FILE__ returns the appropriate info + * + * @return array + */ + public function url_stat() + { + return $this->stat; + } + + /** + * Reads from the stream. + */ + public function stream_read($count) + { + $ret = substr($this->data, $this->pos, $count); + $this->pos += strlen($ret); + return $ret; + } + + + /** + * Tells the current position in the stream. + */ + public function stream_tell() + { + return $this->pos; + } + + + /** + * Tells if we are at the end of the stream. + */ + public function stream_eof() + { + return $this->pos >= strlen($this->data); + } + + + /** + * Stream statistics. + */ + public function stream_stat() + { + return $this->stat; + } + + + /** + * Seek to a specific point in the stream. + */ + public function stream_seek($offset, $whence) + { + switch ($whence) { + case SEEK_SET: + if ($offset < strlen($this->data) && $offset >= 0) { + $this->pos = $offset; + return true; + } else { + return false; + } + break; + + case SEEK_CUR: + if ($offset >= 0) { + $this->pos += $offset; + return true; + } else { + return false; + } + break; + + case SEEK_END: + if (strlen($this->data) + $offset >= 0) { + $this->pos = strlen($this->data) + $offset; + return true; + } else { + return false; + } + break; + + default: + return false; + } + } +} diff --git a/src/Variables.php b/src/Variables.php new file mode 100644 index 00000000..22393eb3 --- /dev/null +++ b/src/Variables.php @@ -0,0 +1,165 @@ +setOptions($options); + } + + /** + * Configure object + * + * @param array $options + * @return Variables + */ + public function setOptions(array $options) + { + foreach ($options as $key => $value) { + switch (strtolower($key)) { + case 'strict_vars': + $this->setStrictVars($value); + break; + default: + // Unknown options are considered variables + $this[$key] = $value; + break; + } + } + return $this; + } + + /** + * Set status of "strict vars" flag + * + * @param bool $flag + * @return Variables + */ + public function setStrictVars($flag) + { + $this->strictVars = (bool) $flag; + return $this; + } + + /** + * Are we operating with strict variables? + * + * @return bool + */ + public function isStrict() + { + return $this->strictVars; + } + + /** + * Assign many values at once + * + * @param array|object $spec + * @return Variables + * @throws Exception\InvalidArgumentException + */ + public function assign($spec) + { + if (is_object($spec)) { + if (method_exists($spec, 'toArray')) { + $spec = $spec->toArray(); + } else { + $spec = (array) $spec; + } + } + if (!is_array($spec)) { + throw new Exception\InvalidArgumentException(sprintf( + 'assign() expects either an array or an object as an argument; received "%s"', + gettype($spec) + )); + } + foreach ($spec as $key => $value) { + $this[$key] = $value; + } + + return $this; + } + + /** + * Get the variable value + * + * If the value has not been defined, a null value will be returned; if + * strict vars on in place, a notice will also be raised. + * + * Otherwise, returns _escaped_ version of the value. + * + * @param mixed $key + * @return void + */ + public function offsetGet($key) + { + if (!$this->offsetExists($key)) { + if ($this->isStrict()) { + trigger_error(sprintf( + 'View variable "%s" does not exist', $key + ), E_USER_NOTICE); + } + return null; + } + + $return = parent::offsetGet($key); + + // If we have a closure/functor, invoke it, and return its return value + if (is_object($return) && is_callable($return)) { + $return = call_user_func($return); + } + + return $return; + } + + /** + * Clear all variables + * + * @return void + */ + public function clear() + { + $this->exchangeArray(array()); + } +} diff --git a/src/View.php b/src/View.php new file mode 100644 index 00000000..0e377127 --- /dev/null +++ b/src/View.php @@ -0,0 +1,261 @@ +request = $request; + return $this; + } + + /** + * Set MVC response object + * + * @param Response $response + * @return View + */ + public function setResponse(Response $response) + { + $this->response = $response; + return $this; + } + + /** + * Get MVC request object + * + * @return null|Request + */ + public function getRequest() + { + return $this->request; + } + + /** + * Get MVC response object + * + * @return null|Response + */ + public function getResponse() + { + return $this->response; + } + + /** + * Set the event manager instance + * + * @param EventManagerInterface $events + * @return View + */ + public function setEventManager(EventManagerInterface $events) + { + $events->setIdentifiers(array( + __CLASS__, + get_called_class(), + )); + $this->events = $events; + return $this; + } + + /** + * Retrieve the event manager instance + * + * Lazy-loads a default instance if none available + * + * @return EventManagerInterface + */ + public function getEventManager() + { + if (!$this->events instanceof EventManagerInterface) { + $this->setEventManager(new EventManager()); + } + return $this->events; + } + + /** + * Add a rendering strategy + * + * Expects a callable. Strategies should accept a ViewEvent object, and should + * return a Renderer instance if the strategy is selected. + * + * Internally, the callable provided will be subscribed to the "renderer" + * event, at the priority specified. + * + * @param callable $callable + * @param int $priority + * @return View + */ + public function addRenderingStrategy($callable, $priority = 1) + { + $this->getEventManager()->attach(ViewEvent::EVENT_RENDERER, $callable, $priority); + return $this; + } + + /** + * Add a response strategy + * + * Expects a callable. Strategies should accept a ViewEvent object. The return + * value will be ignored. + * + * Typical usages for a response strategy are to populate the Response object. + * + * Internally, the callable provided will be subscribed to the "response" + * event, at the priority specified. + * + * @param callable $callable + * @param int $priority + * @return View + */ + public function addResponseStrategy($callable, $priority = 1) + { + $this->getEventManager()->attach(ViewEvent::EVENT_RESPONSE, $callable, $priority); + return $this; + } + + /** + * Render the provided model. + * + * Internally, the following workflow is used: + * + * - Trigger the "renderer" event to select a renderer. + * - Call the selected renderer with the provided Model + * - Trigger the "response" event + * + * @triggers renderer(ViewEvent) + * @triggers response(ViewEvent) + * @param Model $model + * @return void + */ + public function render(Model $model) + { + $event = $this->getEvent(); + $event->setModel($model); + $events = $this->getEventManager(); + $results = $events->trigger(ViewEvent::EVENT_RENDERER, $event, function($result) { + return ($result instanceof Renderer); + }); + $renderer = $results->last(); + if (!$renderer instanceof Renderer) { + throw new Exception\RuntimeException(sprintf( + '%s: no renderer selected!', + __METHOD__ + )); + } + + // If we have children, render them first, but only if: + // a) the renderer does not implement TreeRendererInterface, or + // b) it does, but canRenderTrees() returns false + if ($model->hasChildren() + && (!$renderer instanceof TreeRendererInterface + || !$renderer->canRenderTrees()) + ) { + $this->renderChildren($model); + } + + // Reset the model, in case it has changed, and set the renderer + $event->setModel($model); + $event->setRenderer($renderer); + + $rendered = $renderer->render($model); + + // If this is a child model, return the rendered content; do not + // invoke the response strategy. + $options = $model->getOptions(); + if (array_key_exists('has_parent', $options) && $options['has_parent']) { + return $rendered; + } + + $event->setResult($rendered); + + $events->trigger(ViewEvent::EVENT_RESPONSE, $event); + } + + /** + * Loop through children, rendering each + * + * @param Model $model + * @return void + */ + protected function renderChildren(Model $model) + { + foreach ($model as $child) { + if ($child->terminate()) { + throw new Exception\DomainException('Inconsistent state; child view model is marked as terminal'); + } + $child->setOption('has_parent', true); + $result = $this->render($child); + $child->setOption('has_parent', null); + $capture = $child->captureTo(); + if (!empty($capture)) { + if ($child->isAppend()) { + $oldResult=$model->{$capture}; + $model->setVariable($capture, $oldResult.$result); + } else { + $model->setVariable($capture, $result); + } + } + } + } + + /** + * Create and return ViewEvent used by render() + * + * @return ViewEvent + */ + protected function getEvent() + { + $event = new ViewEvent(); + $event->setTarget($this); + if (null !== ($request = $this->getRequest())) { + $event->setRequest($request); + } + if (null !== ($response = $this->getResponse())) { + $event->setResponse($response); + } + return $event; + } +} diff --git a/src/ViewEvent.php b/src/ViewEvent.php new file mode 100644 index 00000000..b0fad47c --- /dev/null +++ b/src/ViewEvent.php @@ -0,0 +1,262 @@ +model = $model; + return $this; + } + + /** + * Set the MVC request object + * + * @param Request $request + * @return ViewEvent + */ + public function setRequest(Request $request) + { + $this->request = $request; + return $this; + } + + /** + * Set the MVC response object + * + * @param Response $response + * @return ViewEvent + */ + public function setResponse(Response $response) + { + $this->response = $response; + return $this; + } + + /** + * Set result of rendering + * + * @param mixed $result + * @return ViewEvent + */ + public function setResult($result) + { + $this->result = $result; + return $this; + } + + /** + * Retrieve the view model + * + * @return null|Model + */ + public function getModel() + { + return $this->model; + } + + /** + * Set value for renderer + * + * @param Renderer $renderer + * @return ViewEvent + */ + public function setRenderer(Renderer $renderer) + { + $this->renderer = $renderer; + return $this; + } + + /** + * Get value for renderer + * + * @return null|Renderer + */ + public function getRenderer() + { + return $this->renderer; + } + + /** + * Retrieve the MVC request object + * + * @return null|Request + */ + public function getRequest() + { + return $this->request; + } + + /** + * Retrieve the MVC response object + * + * @return null|Response + */ + public function getResponse() + { + return $this->response; + } + + /** + * Retrieve the result of rendering + * + * @return mixed + */ + public function getResult() + { + return $this->result; + } + + /** + * Get event parameter + * + * @param string $name + * @param mixed $default + * @return mixed + */ + public function getParam($name, $default = null) + { + switch ($name) { + case 'model': + return $this->getModel(); + case 'renderer': + return $this->getRenderer(); + case 'request': + return $this->getRequest(); + case 'response': + return $this->getResponse(); + case 'result': + return $this->getResult(); + default: + return parent::getParam($name, $default); + } + } + + /** + * Get all event parameters + * + * @return array|\ArrayAccess + */ + public function getParams() + { + $params = parent::getParams(); + $params['model'] = $this->getModel(); + $params['renderer'] = $this->getRenderer(); + $params['request'] = $this->getRequest(); + $params['response'] = $this->getResponse(); + $params['result'] = $this->getResult(); + return $params; + } + + /** + * Set event parameters + * + * @param array|object|ArrayAccess $params + * @return ViewEvent + */ + public function setParams($params) + { + parent::setParams($params); + if (!is_array($params) && !$params instanceof ArrayAccess) { + return $this; + } + + foreach (array('model', 'renderer', 'request', 'response', 'result') as $param) { + if (isset($params[$param])) { + $method = 'set' . $param; + $this->$method($params[$param]); + } + } + return $this; + } + + /** + * Set an individual event parameter + * + * @param string $name + * @param mixed $value + * @return ViewEvent + */ + public function setParam($name, $value) + { + switch ($name) { + case 'model': + $this->setModel($value); + break; + case 'renderer': + $this->setRenderer($value); + break; + case 'request': + $this->setRequest($value); + break; + case 'response': + $this->setResponse($value); + break; + case 'result': + $this->setResult($value); + break; + default: + parent::setParam($name, $value); + break; + } + return $this; + } +} diff --git a/test/Helper/AbstractTest.php b/test/Helper/AbstractTest.php new file mode 100644 index 00000000..858370e6 --- /dev/null +++ b/test/Helper/AbstractTest.php @@ -0,0 +1,43 @@ +helper = new ConcreteHelper(); + } + + public function testViewSettersGetters() + { + $viewMock = $this->getMock('Zend\View\Renderer\RendererInterface'); + + $this->helper->setView($viewMock); + $this->assertEquals($viewMock, $this->helper->getView()); + } +} diff --git a/test/Helper/BasePathTest.php b/test/Helper/BasePathTest.php new file mode 100644 index 00000000..29cbd840 --- /dev/null +++ b/test/Helper/BasePathTest.php @@ -0,0 +1,56 @@ +setBasePath('/foo'); + + $this->assertEquals('/foo', $helper()); + } + + public function testBasePathWithFile() + { + $helper = new BasePath(); + $helper->setBasePath('/foo'); + + $this->assertEquals('/foo/bar', $helper('bar')); + } + + public function testBasePathNoDoubleSlashes() + { + $helper = new BasePath(); + $helper->setBasePath('/'); + + $this->assertEquals('/', $helper('/')); + } + + public function testBasePathWithFilePrefixedBySlash() + { + $helper = new BasePath(); + $helper->setBasePath('/foo'); + + $this->assertEquals('/foo/bar', $helper('/bar')); + } +} diff --git a/test/Helper/CycleTest.php b/test/Helper/CycleTest.php new file mode 100644 index 00000000..6538f8a8 --- /dev/null +++ b/test/Helper/CycleTest.php @@ -0,0 +1,135 @@ +helper = new Helper\Cycle(); + } + + /** + * Tears down the fixture, for example, close a network connection. + * This method is called after a test is executed. + * + * @return void + */ + public function tearDown() + { + unset($this->helper); + } + + public function testCycleMethodReturnsObjectInstance() + { + $cycle = $this->helper->__invoke(); + $this->assertTrue($cycle instanceof Helper\Cycle); + } + + public function testAssignAndGetValues() + { + $this->helper->assign(array('a', 1, 'asd')); + $this->assertEquals(array('a', 1, 'asd'), $this->helper->getAll()); + } + + public function testCycleMethod() + { + $this->helper->__invoke(array('a', 1, 'asd')); + $this->assertEquals(array('a', 1, 'asd'), $this->helper->getAll()); + } + + public function testToString() + { + $this->helper->__invoke(array('a', 1, 'asd')); + $this->assertEquals('a', (string) $this->helper->toString()); + } + + public function testNextValue() + { + $this->helper->assign(array('a', 1, 3)); + $this->assertEquals('a', (string) $this->helper->next()); + $this->assertEquals(1, (string) $this->helper->next()); + $this->assertEquals(3, (string) $this->helper->next()); + $this->assertEquals('a', (string) $this->helper->next()); + $this->assertEquals(1, (string) $this->helper->next()); + } + + public function testPrevValue() + { + $this->helper->assign(array(4, 1, 3)); + $this->assertEquals(3, (string) $this->helper->prev()); + $this->assertEquals(1, (string) $this->helper->prev()); + $this->assertEquals(4, (string) $this->helper->prev()); + $this->assertEquals(3, (string) $this->helper->prev()); + $this->assertEquals(1, (string) $this->helper->prev()); + } + + public function testRewind() + { + $this->helper->assign(array(5, 8, 3)); + $this->assertEquals(5, (string) $this->helper->next()); + $this->assertEquals(8, (string) $this->helper->next()); + $this->helper->rewind(); + $this->assertEquals(5, (string) $this->helper->next()); + $this->assertEquals(8, (string) $this->helper->next()); + } + + public function testMixedMethods() + { + $this->helper->assign(array(5, 8, 3)); + $this->assertEquals(5, (string) $this->helper->next()); + $this->assertEquals(5, (string) $this->helper->current()); + $this->assertEquals(8, (string) $this->helper->next()); + $this->assertEquals(5, (string) $this->helper->prev()); + } + + public function testTwoCycles() + { + $this->helper->assign(array(5, 8, 3)); + $this->assertEquals(5, (string) $this->helper->next()); + $this->assertEquals(2, (string) $this->helper->__invoke(array(2,38,1),'cycle2')->next()); + $this->assertEquals(8, (string) $this->helper->__invoke()->next()); + $this->assertEquals(38, (string) $this->helper->setName('cycle2')->next()); + } + + public function testTwoCyclesInLoop() + { + $expected = array(5,4,2,3); + $expected2 = array(7,34,8,6); + for ($i=0;$i<4;$i++) { + $this->assertEquals($expected[$i], (string) $this->helper->__invoke($expected)->next()); + $this->assertEquals($expected2[$i], (string) $this->helper->__invoke($expected2,'cycle2')->next()); + } + } + +} diff --git a/test/Helper/DeclareVarsTest.php b/test/Helper/DeclareVarsTest.php new file mode 100644 index 00000000..93d1dcc0 --- /dev/null +++ b/test/Helper/DeclareVarsTest.php @@ -0,0 +1,84 @@ +resolver()->addPath(__DIR__ . $base); + $view->vars()->setStrictVars(true); + $this->view = $view; + } + + public function tearDown() + { + unset($this->view); + } + + protected function _declareVars() + { + $this->view->plugin('declareVars')->__invoke( + 'varName1', + 'varName2', + array( + 'varName3' => 'defaultValue', + 'varName4' => array() + ) + ); + } + + public function testDeclareUndeclaredVars() + { + $this->_declareVars(); + + $vars = $this->view->vars(); + $this->assertTrue(isset($vars->varName1)); + $this->assertTrue(isset($vars->varName2)); + $this->assertTrue(isset($vars->varName3)); + $this->assertTrue(isset($vars->varName4)); + + $this->assertEquals('defaultValue', $vars->varName3); + $this->assertEquals(array(), $vars->varName4); + } + + public function testDeclareDeclaredVars() + { + $vars = $this->view->vars(); + $vars->varName2 = 'alreadySet'; + $vars->varName3 = 'myValue'; + $vars->varName5 = 'additionalValue'; + + $this->_declareVars(); + + $this->assertTrue(isset($vars->varName1)); + $this->assertTrue(isset($vars->varName2)); + $this->assertTrue(isset($vars->varName3)); + $this->assertTrue(isset($vars->varName4)); + $this->assertTrue(isset($vars->varName5)); + + $this->assertEquals('alreadySet', $vars->varName2); + $this->assertEquals('myValue', $vars->varName3); + $this->assertEquals('additionalValue', $vars->varName5); + } +} diff --git a/test/Helper/DoctypeTest.php b/test/Helper/DoctypeTest.php new file mode 100644 index 00000000..ec660ba3 --- /dev/null +++ b/test/Helper/DoctypeTest.php @@ -0,0 +1,205 @@ +helper = new Helper\Doctype(); + } + + /** + * Tears down the fixture, for example, close a network connection. + * This method is called after a test is executed. + * + * @return void + */ + public function tearDown() + { + unset($this->helper); + } + + public function testDoctypeMethodReturnsObjectInstance() + { + $doctype = $this->helper->__invoke(); + $this->assertTrue($doctype instanceof Helper\Doctype); + } + + public function testPassingDoctypeSetsDoctype() + { + $doctype = $this->helper->__invoke(Helper\Doctype::XHTML1_STRICT); + $this->assertEquals(Helper\Doctype::XHTML1_STRICT, $doctype->getDoctype()); + } + + public function testIsXhtmlReturnsTrueForXhtmlDoctypes() + { + $types = array( + Helper\Doctype::XHTML1_STRICT, + Helper\Doctype::XHTML1_TRANSITIONAL, + Helper\Doctype::XHTML1_FRAMESET, + Helper\Doctype::XHTML1_RDFA, + Helper\Doctype::XHTML1_RDFA11, + Helper\Doctype::XHTML5 + ); + + foreach ($types as $type) { + $doctype = $this->helper->__invoke($type); + $this->assertEquals($type, $doctype->getDoctype()); + $this->assertTrue($doctype->isXhtml()); + } + + $doctype = $this->helper->__invoke(''); + $this->assertEquals('CUSTOM_XHTML', $doctype->getDoctype()); + $this->assertTrue($doctype->isXhtml()); + } + + public function testIsXhtmlReturnsFalseForNonXhtmlDoctypes() + { + $types = array( + Helper\Doctype::HTML4_STRICT, + Helper\Doctype::HTML4_LOOSE, + Helper\Doctype::HTML4_FRAMESET, + ); + + foreach ($types as $type) { + $doctype = $this->helper->__invoke($type); + $this->assertEquals($type, $doctype->getDoctype()); + $this->assertFalse($doctype->isXhtml()); + } + + $doctype = $this->helper->__invoke(''); + $this->assertEquals('CUSTOM', $doctype->getDoctype()); + $this->assertFalse($doctype->isXhtml()); + } + + public function testIsHtml5() + { + foreach (array(Helper\Doctype::HTML5, Helper\Doctype::XHTML5) as $type) { + $doctype = $this->helper->__invoke($type); + $this->assertEquals($type, $doctype->getDoctype()); + $this->assertTrue($doctype->isHtml5()); + } + + $types = array( + Helper\Doctype::HTML4_STRICT, + Helper\Doctype::HTML4_LOOSE, + Helper\Doctype::HTML4_FRAMESET, + Helper\Doctype::XHTML1_STRICT, + Helper\Doctype::XHTML1_TRANSITIONAL, + Helper\Doctype::XHTML1_FRAMESET + ); + + + foreach ($types as $type) { + $doctype = $this->helper->__invoke($type); + $this->assertEquals($type, $doctype->getDoctype()); + $this->assertFalse($doctype->isHtml5()); + } + } + + public function testIsRdfa() + { + // ensure default registerd Doctype is false + $this->assertFalse($this->helper->isRdfa()); + + $this->assertTrue($this->helper->__invoke(Helper\Doctype::XHTML1_RDFA)->isRdfa()); + $this->assertTrue($this->helper->__invoke(Helper\Doctype::XHTML1_RDFA11)->isRdfa()); + $this->assertTrue($this->helper->__invoke(Helper\Doctype::XHTML5)->isRdfa()); + $this->assertTrue($this->helper->__invoke(Helper\Doctype::HTML5)->isRdfa()); + + // build-in doctypes + $doctypes = array( + Helper\Doctype::XHTML11, + Helper\Doctype::XHTML1_STRICT, + Helper\Doctype::XHTML1_TRANSITIONAL, + Helper\Doctype::XHTML1_FRAMESET, + Helper\Doctype::XHTML_BASIC1, + Helper\Doctype::HTML4_STRICT, + Helper\Doctype::HTML4_LOOSE, + Helper\Doctype::HTML4_FRAMESET, + ); + + foreach ($doctypes as $type) { + $this->assertFalse($this->helper->__invoke($type)->isRdfa()); + } + + // custom doctype + $doctype = $this->helper->__invoke(''); + $this->assertFalse($doctype->isRdfa()); + } + + public function testCanRegisterCustomHtml5Doctype() + { + $doctype = $this->helper->__invoke(''); + $this->assertEquals('CUSTOM', $doctype->getDoctype()); + $this->assertTrue($doctype->isHtml5()); + } + + public function testCanRegisterCustomXhtmlDoctype() + { + $doctype = $this->helper->__invoke(''); + $this->assertEquals('CUSTOM_XHTML', $doctype->getDoctype()); + $this->assertTrue($doctype->isXhtml()); + } + + public function testCanRegisterCustomHtmlDoctype() + { + $doctype = $this->helper->__invoke(''); + $this->assertEquals('CUSTOM', $doctype->getDoctype()); + $this->assertFalse($doctype->isXhtml()); + } + + public function testMalformedCustomDoctypeRaisesException() + { + try { + $doctype = $this->helper->__invoke(''); + $this->fail('Malformed doctype should raise exception'); + } catch (\Exception $e) { + } + } + + public function testStringificationReturnsDoctypeString() + { + $doctype = $this->helper->__invoke(Helper\Doctype::XHTML1_STRICT); + $string = $doctype->__toString(); + $this->assertEquals('', $string); + } +} + diff --git a/test/Helper/EscapeCssTest.php b/test/Helper/EscapeCssTest.php new file mode 100644 index 00000000..29014ac0 --- /dev/null +++ b/test/Helper/EscapeCssTest.php @@ -0,0 +1,203 @@ +helper = new EscapeHelper; + } + + public function testUsesUtf8EncodingByDefault() + { + $this->assertEquals('UTF-8', $this->helper->getEncoding()); + } + + /** + * @expectedException \Zend\View\Exception\InvalidArgumentException + */ + public function testEncodingIsImmutable() + { + $this->helper->setEncoding('BIG5-HKSCS'); + $this->helper->getEscaper(); + $this->helper->setEncoding('UTF-8'); + } + + public function testGetEscaperCreatesDefaultInstanceWithCorrectEncoding() + { + $this->helper->setEncoding('BIG5-HKSCS'); + $escaper = $this->helper->getEscaper(); + $this->assertTrue($escaper instanceof \Zend\Escaper\Escaper); + $this->assertEquals('big5-hkscs', $escaper->getEncoding()); + } + + public function testSettingEscaperObjectAlsoSetsEncoding() + { + $escaper = new \Zend\Escaper\Escaper('big5-hkscs'); + $this->helper->setEscaper($escaper); + $escaper = $this->helper->getEscaper(); + $this->assertTrue($escaper instanceof \Zend\Escaper\Escaper); + $this->assertEquals('big5-hkscs', $escaper->getEncoding()); + } + + public function testEscapehtmlCalledOnEscaperObject() + { + $escaper = $this->getMock('\\Zend\\Escaper\\Escaper'); + $escaper->expects($this->any())->method('escapeCss'); + $this->helper->setEscaper($escaper); + $this->helper->__invoke('foo'); + } + + public function testAllowsRecursiveEscapingOfArrays() + { + $original = array( + 'foo' => 'bar', + 'baz' => array( + 'bat', + 'second' => array( + 'third', + ), + ), + ); + $expected = array( + 'foo' => '\3C b\3E bar\3C \2F b\3E ', + 'baz' => array( + '\3C em\3E bat\3C \2F em\3E ', + 'second' => array( + '\3C i\3E third\3C \2F i\3E ', + ), + ), + ); + $test = $this->helper->__invoke($original, EscapeHelper::RECURSE_ARRAY); + $this->assertEquals($expected, $test); + } + + public function testWillCastObjectsToStringsBeforeEscaping() + { + $object = new TestAsset\Stringified; + $test = $this->helper->__invoke($object); + $this->assertEquals( + 'ZendTest\5C View\5C Helper\5C TestAsset\5C Stringified', + $test + ); + } + + public function testCanRecurseObjectImplementingToArray() + { + $original = array( + 'foo' => 'bar', + 'baz' => array( + 'bat', + 'second' => array( + 'third', + ), + ), + ); + $object = new TestAsset\ToArray(); + $object->array = $original; + + $expected = array( + 'foo' => '\3C b\3E bar\3C \2F b\3E ', + 'baz' => array( + '\3C em\3E bat\3C \2F em\3E ', + 'second' => array( + '\3C i\3E third\3C \2F i\3E ', + ), + ), + ); + $test = $this->helper->__invoke($object, EscapeHelper::RECURSE_OBJECT); + $this->assertEquals($expected, $test); + } + + public function testCanRecurseObjectProperties() + { + $original = array( + 'foo' => 'bar', + 'baz' => array( + 'bat', + 'second' => array( + 'third', + ), + ), + ); + $object = new stdClass(); + foreach ($original as $key => $value) { + $object->$key = $value; + } + + $expected = array( + 'foo' => '\3C b\3E bar\3C \2F b\3E ', + 'baz' => array( + '\3C em\3E bat\3C \2F em\3E ', + 'second' => array( + '\3C i\3E third\3C \2F i\3E ', + ), + ), + ); + $test = $this->helper->__invoke($object, EscapeHelper::RECURSE_OBJECT); + $this->assertEquals($expected, $test); + } + + /** + * @expectedException \Zend\Escaper\Exception\InvalidArgumentException + * + * PHP 5.3 instates default encoding on empty string instead of the expected + * warning level error for htmlspecialchars() encoding param. PHP 5.4 attempts + * to guess the encoding or take it from php.ini default_charset when an empty + * string is set. Both are insecure behaviours. + */ + public function testSettingEncodingToEmptyStringShouldThrowException() + { + $this->helper->setEncoding(''); + $this->helper->getEscaper(); + } + + public function testSettingValidEncodingShouldNotThrowExceptions() + { + foreach ($this->supportedEncodings as $value) { + $helper = new EscapeHelper; + $helper->setEncoding($value); + $helper->getEscaper(); + } + } + + /** + * @expectedException \Zend\Escaper\Exception\InvalidArgumentException + * + * All versions of PHP - when an invalid encoding is set on htmlspecialchars() + * a warning level error is issued and escaping continues with the default encoding + * for that PHP version. Preventing the continuation behaviour offsets display_errors + * off in production env. + */ + public function testSettingEncodingToInvalidValueShouldThrowException() + { + $this->helper->setEncoding('completely-invalid'); + $this->helper->getEscaper(); + } +} diff --git a/test/Helper/EscapeHtmlAttrTest.php b/test/Helper/EscapeHtmlAttrTest.php new file mode 100644 index 00000000..09f663f5 --- /dev/null +++ b/test/Helper/EscapeHtmlAttrTest.php @@ -0,0 +1,203 @@ +helper = new EscapeHelper; + } + + public function testUsesUtf8EncodingByDefault() + { + $this->assertEquals('UTF-8', $this->helper->getEncoding()); + } + + /** + * @expectedException \Zend\View\Exception\InvalidArgumentException + */ + public function testEncodingIsImmutable() + { + $this->helper->setEncoding('BIG5-HKSCS'); + $this->helper->getEscaper(); + $this->helper->setEncoding('UTF-8'); + } + + public function testGetEscaperCreatesDefaultInstanceWithCorrectEncoding() + { + $this->helper->setEncoding('BIG5-HKSCS'); + $escaper = $this->helper->getEscaper(); + $this->assertTrue($escaper instanceof \Zend\Escaper\Escaper); + $this->assertEquals('big5-hkscs', $escaper->getEncoding()); + } + + public function testSettingEscaperObjectAlsoSetsEncoding() + { + $escaper = new \Zend\Escaper\Escaper('big5-hkscs'); + $this->helper->setEscaper($escaper); + $escaper = $this->helper->getEscaper(); + $this->assertTrue($escaper instanceof \Zend\Escaper\Escaper); + $this->assertEquals('big5-hkscs', $escaper->getEncoding()); + } + + public function testEscapehtmlCalledOnEscaperObject() + { + $escaper = $this->getMock('\\Zend\\Escaper\\Escaper'); + $escaper->expects($this->any())->method('escapeHtmlAttr'); + $this->helper->setEscaper($escaper); + $this->helper->__invoke('foo'); + } + + public function testAllowsRecursiveEscapingOfArrays() + { + $original = array( + 'foo' => 'bar', + 'baz' => array( + 'bat', + 'second' => array( + 'third', + ), + ), + ); + $expected = array( + 'foo' => '<b>bar</b>', + 'baz' => array( + '<em>bat</em>', + 'second' => array( + '<i>third</i>', + ), + ), + ); + $test = $this->helper->__invoke($original, EscapeHelper::RECURSE_ARRAY); + $this->assertEquals($expected, $test); + } + + public function testWillCastObjectsToStringsBeforeEscaping() + { + $object = new TestAsset\Stringified; + $test = $this->helper->__invoke($object); + $this->assertEquals( + 'ZendTest\View\Helper\TestAsset\Stringified', + $test + ); + } + + public function testCanRecurseObjectImplementingToArray() + { + $original = array( + 'foo' => 'bar', + 'baz' => array( + 'bat', + 'second' => array( + 'third', + ), + ), + ); + $object = new TestAsset\ToArray(); + $object->array = $original; + + $expected = array( + 'foo' => '<b>bar</b>', + 'baz' => array( + '<em>bat</em>', + 'second' => array( + '<i>third</i>', + ), + ), + ); + $test = $this->helper->__invoke($object, EscapeHelper::RECURSE_OBJECT); + $this->assertEquals($expected, $test); + } + + public function testCanRecurseObjectProperties() + { + $original = array( + 'foo' => 'bar', + 'baz' => array( + 'bat', + 'second' => array( + 'third', + ), + ), + ); + $object = new stdClass(); + foreach ($original as $key => $value) { + $object->$key = $value; + } + + $expected = array( + 'foo' => '<b>bar</b>', + 'baz' => array( + '<em>bat</em>', + 'second' => array( + '<i>third</i>', + ), + ), + ); + $test = $this->helper->__invoke($object, EscapeHelper::RECURSE_OBJECT); + $this->assertEquals($expected, $test); + } + + /** + * @expectedException \Zend\Escaper\Exception\InvalidArgumentException + * + * PHP 5.3 instates default encoding on empty string instead of the expected + * warning level error for htmlspecialchars() encoding param. PHP 5.4 attempts + * to guess the encoding or take it from php.ini default_charset when an empty + * string is set. Both are insecure behaviours. + */ + public function testSettingEncodingToEmptyStringShouldThrowException() + { + $this->helper->setEncoding(''); + $this->helper->getEscaper(); + } + + public function testSettingValidEncodingShouldNotThrowExceptions() + { + foreach ($this->supportedEncodings as $value) { + $helper = new EscapeHelper; + $helper->setEncoding($value); + $helper->getEscaper(); + } + } + + /** + * @expectedException \Zend\Escaper\Exception\InvalidArgumentException + * + * All versions of PHP - when an invalid encoding is set on htmlspecialchars() + * a warning level error is issued and escaping continues with the default encoding + * for that PHP version. Preventing the continuation behaviour offsets display_errors + * off in production env. + */ + public function testSettingEncodingToInvalidValueShouldThrowException() + { + $this->helper->setEncoding('completely-invalid'); + $this->helper->getEscaper(); + } +} diff --git a/test/Helper/EscapeHtmlTest.php b/test/Helper/EscapeHtmlTest.php new file mode 100644 index 00000000..6a81493b --- /dev/null +++ b/test/Helper/EscapeHtmlTest.php @@ -0,0 +1,200 @@ +helper = new EscapeHelper; + } + + public function testUsesUtf8EncodingByDefault() + { + $this->assertEquals('UTF-8', $this->helper->getEncoding()); + } + + /** + * @expectedException \Zend\View\Exception\InvalidArgumentException + */ + public function testEncodingIsImmutable() + { + $this->helper->setEncoding('BIG5-HKSCS'); + $this->helper->getEscaper(); + $this->helper->setEncoding('UTF-8'); + } + + public function testGetEscaperCreatesDefaultInstanceWithCorrectEncoding() + { + $this->helper->setEncoding('BIG5-HKSCS'); + $escaper = $this->helper->getEscaper(); + $this->assertTrue($escaper instanceof \Zend\Escaper\Escaper); + $this->assertEquals('big5-hkscs', $escaper->getEncoding()); + } + + public function testSettingEscaperObjectAlsoSetsEncoding() + { + $escaper = new \Zend\Escaper\Escaper('big5-hkscs'); + $this->helper->setEscaper($escaper); + $escaper = $this->helper->getEscaper(); + $this->assertTrue($escaper instanceof \Zend\Escaper\Escaper); + $this->assertEquals('big5-hkscs', $escaper->getEncoding()); + } + + public function testEscapehtmlCalledOnEscaperObject() + { + $escaper = $this->getMock('\\Zend\\Escaper\\Escaper'); + $escaper->expects($this->any())->method('escapeHtml'); + $this->helper->setEscaper($escaper); + $this->helper->__invoke('foo'); + } + + public function testAllowsRecursiveEscapingOfArrays() + { + $original = array( + 'foo' => 'bar', + 'baz' => array( + 'bat', + 'second' => array( + 'third', + ), + ), + ); + $expected = array( + 'foo' => '<b>bar</b>', + 'baz' => array( + '<em>bat</em>', + 'second' => array( + '<i>third</i>', + ), + ), + ); + $test = $this->helper->__invoke($original, EscapeHelper::RECURSE_ARRAY); + $this->assertEquals($expected, $test); + } + + public function testWillCastObjectsToStringsBeforeEscaping() + { + $object = new TestAsset\Stringified; + $test = $this->helper->__invoke($object); + $this->assertEquals(get_class($object), $test); + } + + public function testCanRecurseObjectImplementingToArray() + { + $original = array( + 'foo' => 'bar', + 'baz' => array( + 'bat', + 'second' => array( + 'third', + ), + ), + ); + $object = new TestAsset\ToArray(); + $object->array = $original; + + $expected = array( + 'foo' => '<b>bar</b>', + 'baz' => array( + '<em>bat</em>', + 'second' => array( + '<i>third</i>', + ), + ), + ); + $test = $this->helper->__invoke($object, EscapeHelper::RECURSE_OBJECT); + $this->assertEquals($expected, $test); + } + + public function testCanRecurseObjectProperties() + { + $original = array( + 'foo' => 'bar', + 'baz' => array( + 'bat', + 'second' => array( + 'third', + ), + ), + ); + $object = new stdClass(); + foreach ($original as $key => $value) { + $object->$key = $value; + } + + $expected = array( + 'foo' => '<b>bar</b>', + 'baz' => array( + '<em>bat</em>', + 'second' => array( + '<i>third</i>', + ), + ), + ); + $test = $this->helper->__invoke($object, EscapeHelper::RECURSE_OBJECT); + $this->assertEquals($expected, $test); + } + + /** + * @expectedException \Zend\Escaper\Exception\InvalidArgumentException + * + * PHP 5.3 instates default encoding on empty string instead of the expected + * warning level error for htmlspecialchars() encoding param. PHP 5.4 attempts + * to guess the encoding or take it from php.ini default_charset when an empty + * string is set. Both are insecure behaviours. + */ + public function testSettingEncodingToEmptyStringShouldThrowException() + { + $this->helper->setEncoding(''); + $this->helper->getEscaper(); + } + + public function testSettingValidEncodingShouldNotThrowExceptions() + { + foreach ($this->supportedEncodings as $value) { + $helper = new EscapeHelper; + $helper->setEncoding($value); + $helper->getEscaper(); + } + } + + /** + * @expectedException \Zend\Escaper\Exception\InvalidArgumentException + * + * All versions of PHP - when an invalid encoding is set on htmlspecialchars() + * a warning level error is issued and escaping continues with the default encoding + * for that PHP version. Preventing the continuation behaviour offsets display_errors + * off in production env. + */ + public function testSettingEncodingToInvalidValueShouldThrowException() + { + $this->helper->setEncoding('completely-invalid'); + $this->helper->getEscaper(); + } +} diff --git a/test/Helper/EscapeJsTest.php b/test/Helper/EscapeJsTest.php new file mode 100644 index 00000000..34c7e460 --- /dev/null +++ b/test/Helper/EscapeJsTest.php @@ -0,0 +1,203 @@ +helper = new EscapeHelper; + } + + public function testUsesUtf8EncodingByDefault() + { + $this->assertEquals('UTF-8', $this->helper->getEncoding()); + } + + /** + * @expectedException \Zend\View\Exception\InvalidArgumentException + */ + public function testEncodingIsImmutable() + { + $this->helper->setEncoding('BIG5-HKSCS'); + $this->helper->getEscaper(); + $this->helper->setEncoding('UTF-8'); + } + + public function testGetEscaperCreatesDefaultInstanceWithCorrectEncoding() + { + $this->helper->setEncoding('BIG5-HKSCS'); + $escaper = $this->helper->getEscaper(); + $this->assertTrue($escaper instanceof \Zend\Escaper\Escaper); + $this->assertEquals('big5-hkscs', $escaper->getEncoding()); + } + + public function testSettingEscaperObjectAlsoSetsEncoding() + { + $escaper = new \Zend\Escaper\Escaper('big5-hkscs'); + $this->helper->setEscaper($escaper); + $escaper = $this->helper->getEscaper(); + $this->assertTrue($escaper instanceof \Zend\Escaper\Escaper); + $this->assertEquals('big5-hkscs', $escaper->getEncoding()); + } + + public function testEscapehtmlCalledOnEscaperObject() + { + $escaper = $this->getMock('\\Zend\\Escaper\\Escaper'); + $escaper->expects($this->any())->method('escapeJs'); + $this->helper->setEscaper($escaper); + $this->helper->__invoke('foo'); + } + + public function testAllowsRecursiveEscapingOfArrays() + { + $original = array( + 'foo' => 'bar', + 'baz' => array( + 'bat', + 'second' => array( + 'third', + ), + ), + ); + $expected = array( + 'foo' => '\x3Cb\x3Ebar\x3C\x2Fb\x3E', + 'baz' => array( + '\x3Cem\x3Ebat\x3C\x2Fem\x3E', + 'second' => array( + '\x3Ci\x3Ethird\x3C\x2Fi\x3E', + ), + ), + ); + $test = $this->helper->__invoke($original, EscapeHelper::RECURSE_ARRAY); + $this->assertEquals($expected, $test); + } + + public function testWillCastObjectsToStringsBeforeEscaping() + { + $object = new TestAsset\Stringified; + $test = $this->helper->__invoke($object); + $this->assertEquals( + 'ZendTest\x5CView\x5CHelper\x5CTestAsset\x5CStringified', + $test + ); + } + + public function testCanRecurseObjectImplementingToArray() + { + $original = array( + 'foo' => 'bar', + 'baz' => array( + 'bat', + 'second' => array( + 'third', + ), + ), + ); + $object = new TestAsset\ToArray(); + $object->array = $original; + + $expected = array( + 'foo' => '\x3Cb\x3Ebar\x3C\x2Fb\x3E', + 'baz' => array( + '\x3Cem\x3Ebat\x3C\x2Fem\x3E', + 'second' => array( + '\x3Ci\x3Ethird\x3C\x2Fi\x3E', + ), + ), + ); + $test = $this->helper->__invoke($object, EscapeHelper::RECURSE_OBJECT); + $this->assertEquals($expected, $test); + } + + public function testCanRecurseObjectProperties() + { + $original = array( + 'foo' => 'bar', + 'baz' => array( + 'bat', + 'second' => array( + 'third', + ), + ), + ); + $object = new stdClass(); + foreach ($original as $key => $value) { + $object->$key = $value; + } + + $expected = array( + 'foo' => '\x3Cb\x3Ebar\x3C\x2Fb\x3E', + 'baz' => array( + '\x3Cem\x3Ebat\x3C\x2Fem\x3E', + 'second' => array( + '\x3Ci\x3Ethird\x3C\x2Fi\x3E', + ), + ), + ); + $test = $this->helper->__invoke($object, EscapeHelper::RECURSE_OBJECT); + $this->assertEquals($expected, $test); + } + + /** + * @expectedException \Zend\Escaper\Exception\InvalidArgumentException + * + * PHP 5.3 instates default encoding on empty string instead of the expected + * warning level error for htmlspecialchars() encoding param. PHP 5.4 attempts + * to guess the encoding or take it from php.ini default_charset when an empty + * string is set. Both are insecure behaviours. + */ + public function testSettingEncodingToEmptyStringShouldThrowException() + { + $this->helper->setEncoding(''); + $this->helper->getEscaper(); + } + + public function testSettingValidEncodingShouldNotThrowExceptions() + { + foreach ($this->supportedEncodings as $value) { + $helper = new EscapeHelper; + $helper->setEncoding($value); + $helper->getEscaper(); + } + } + + /** + * @expectedException \Zend\Escaper\Exception\InvalidArgumentException + * + * All versions of PHP - when an invalid encoding is set on htmlspecialchars() + * a warning level error is issued and escaping continues with the default encoding + * for that PHP version. Preventing the continuation behaviour offsets display_errors + * off in production env. + */ + public function testSettingEncodingToInvalidValueShouldThrowException() + { + $this->helper->setEncoding('completely-invalid'); + $this->helper->getEscaper(); + } +} diff --git a/test/Helper/EscapeUrlTest.php b/test/Helper/EscapeUrlTest.php new file mode 100644 index 00000000..a69ef612 --- /dev/null +++ b/test/Helper/EscapeUrlTest.php @@ -0,0 +1,203 @@ +helper = new EscapeHelper; + } + + public function testUsesUtf8EncodingByDefault() + { + $this->assertEquals('UTF-8', $this->helper->getEncoding()); + } + + /** + * @expectedException \Zend\View\Exception\InvalidArgumentException + */ + public function testEncodingIsImmutable() + { + $this->helper->setEncoding('BIG5-HKSCS'); + $this->helper->getEscaper(); + $this->helper->setEncoding('UTF-8'); + } + + public function testGetEscaperCreatesDefaultInstanceWithCorrectEncoding() + { + $this->helper->setEncoding('BIG5-HKSCS'); + $escaper = $this->helper->getEscaper(); + $this->assertTrue($escaper instanceof \Zend\Escaper\Escaper); + $this->assertEquals('big5-hkscs', $escaper->getEncoding()); + } + + public function testSettingEscaperObjectAlsoSetsEncoding() + { + $escaper = new \Zend\Escaper\Escaper('big5-hkscs'); + $this->helper->setEscaper($escaper); + $escaper = $this->helper->getEscaper(); + $this->assertTrue($escaper instanceof \Zend\Escaper\Escaper); + $this->assertEquals('big5-hkscs', $escaper->getEncoding()); + } + + public function testEscapehtmlCalledOnEscaperObject() + { + $escaper = $this->getMock('\\Zend\\Escaper\\Escaper'); + $escaper->expects($this->any())->method('escapeUrl'); + $this->helper->setEscaper($escaper); + $this->helper->__invoke('foo'); + } + + public function testAllowsRecursiveEscapingOfArrays() + { + $original = array( + 'foo' => 'bar', + 'baz' => array( + 'bat', + 'second' => array( + 'third', + ), + ), + ); + $expected = array( + 'foo' => '%3Cb%3Ebar%3C%2Fb%3E', + 'baz' => array( + '%3Cem%3Ebat%3C%2Fem%3E', + 'second' => array( + '%3Ci%3Ethird%3C%2Fi%3E', + ), + ), + ); + $test = $this->helper->__invoke($original, EscapeHelper::RECURSE_ARRAY); + $this->assertEquals($expected, $test); + } + + public function testWillCastObjectsToStringsBeforeEscaping() + { + $object = new TestAsset\Stringified; + $test = $this->helper->__invoke($object); + $this->assertEquals( + 'ZendTest%5CView%5CHelper%5CTestAsset%5CStringified', + $test + ); + } + + public function testCanRecurseObjectImplementingToArray() + { + $original = array( + 'foo' => 'bar', + 'baz' => array( + 'bat', + 'second' => array( + 'third', + ), + ), + ); + $object = new TestAsset\ToArray(); + $object->array = $original; + + $expected = array( + 'foo' => '%3Cb%3Ebar%3C%2Fb%3E', + 'baz' => array( + '%3Cem%3Ebat%3C%2Fem%3E', + 'second' => array( + '%3Ci%3Ethird%3C%2Fi%3E', + ), + ), + ); + $test = $this->helper->__invoke($object, EscapeHelper::RECURSE_OBJECT); + $this->assertEquals($expected, $test); + } + + public function testCanRecurseObjectProperties() + { + $original = array( + 'foo' => 'bar', + 'baz' => array( + 'bat', + 'second' => array( + 'third', + ), + ), + ); + $object = new stdClass(); + foreach ($original as $key => $value) { + $object->$key = $value; + } + + $expected = array( + 'foo' => '%3Cb%3Ebar%3C%2Fb%3E', + 'baz' => array( + '%3Cem%3Ebat%3C%2Fem%3E', + 'second' => array( + '%3Ci%3Ethird%3C%2Fi%3E', + ), + ), + ); + $test = $this->helper->__invoke($object, EscapeHelper::RECURSE_OBJECT); + $this->assertEquals($expected, $test); + } + + /** + * @expectedException \Zend\Escaper\Exception\InvalidArgumentException + * + * PHP 5.3 instates default encoding on empty string instead of the expected + * warning level error for htmlspecialchars() encoding param. PHP 5.4 attempts + * to guess the encoding or take it from php.ini default_charset when an empty + * string is set. Both are insecure behaviours. + */ + public function testSettingEncodingToEmptyStringShouldThrowException() + { + $this->helper->setEncoding(''); + $this->helper->getEscaper(); + } + + public function testSettingValidEncodingShouldNotThrowExceptions() + { + foreach ($this->supportedEncodings as $value) { + $helper = new EscapeHelper; + $helper->setEncoding($value); + $helper->getEscaper(); + } + } + + /** + * @expectedException \Zend\Escaper\Exception\InvalidArgumentException + * + * All versions of PHP - when an invalid encoding is set on htmlspecialchars() + * a warning level error is issued and escaping continues with the default encoding + * for that PHP version. Preventing the continuation behaviour offsets display_errors + * off in production env. + */ + public function testSettingEncodingToInvalidValueShouldThrowException() + { + $this->helper->setEncoding('completely-invalid'); + $this->helper->getEscaper(); + } +} diff --git a/test/Helper/GravatarTest.php b/test/Helper/GravatarTest.php new file mode 100644 index 00000000..a4ae0a81 --- /dev/null +++ b/test/Helper/GravatarTest.php @@ -0,0 +1,265 @@ +helper = new Gravatar(); + $this->view = new View(); + $this->view->doctype()->setDoctype(strtoupper("XHTML1_STRICT")); + $this->helper->setView($this->view); + + if (isset($_SERVER['HTTPS'])) { + unset ($_SERVER['HTTPS']); + } + } + + /** + * Cleans up the environment after running a test. + */ + protected function tearDown() + { + unset($this->helper, $this->view); + } + + /** + * Test default options. + */ + public function testGravatarXhtmlDoctype() + { + $this->assertRegExp( + '/\/>$/', + $this->helper->__invoke('example@example.com')->__toString() + ); + } + + /** + * Test if doctype is HTML + */ + public function testGravatarHtmlDoctype() + { + $object = new Gravatar(); + $view = new View(); + $view->doctype()->setDoctype(strtoupper("HTML5")); + $object->setView($view); + + $this->assertRegExp( + '/[^\/]>$/', + $this->helper->__invoke('example@example.com')->__toString() + ); + } + + /** + * Test get set methods + */ + public function testGetAndSetMethods() + { + $attribs = array('class' => 'gravatar', 'title' => 'avatar', 'id' => 'gravatar-1'); + $this->helper->setDefaultImg('monsterid') + ->setImgSize(150) + ->setSecure(true) + ->setEmail("example@example.com") + ->setAttribs($attribs) + ->setRating('pg'); + $this->assertEquals("monsterid", $this->helper->getDefaultImg()); + $this->assertEquals("pg", $this->helper->getRating()); + $this->assertEquals("example@example.com", $this->helper->getEmail()); + $this->assertEquals($attribs, $this->helper->getAttribs()); + $this->assertEquals(150, $this->helper->getImgSize()); + $this->assertTrue($this->helper->getSecure()); + } + + public function tesSetDefaultImg() + { + $this->helper->gravatar("example@example.com"); + + $img = array( + "wavatar", + "http://www.example.com/images/avatar/example.png", + Gravatar::DEFAULT_MONSTERID, + ); + + foreach ($img as $value) { + $this->helper->setDefaultImg($value); + $this->assertEquals(urlencode($value), $this->helper->getDefaultImg()); + } + } + + public function testSetImgSize() + { + $imgSizesRight = array(1, 500, "600"); + foreach ($imgSizesRight as $value) { + $this->helper->setImgSize($value); + $this->assertInternalType('int', $this->helper->getImgSize()); + } + } + + public function testInvalidRatingParametr() + { + $ratingsWrong = array( 'a', 'cs', 456); + $this->setExpectedException('Zend\View\Exception\ExceptionInterface'); + foreach ($ratingsWrong as $value) { + $this->helper->setRating($value); + } + } + + public function testSetRating() + { + $ratingsRight = array( 'g', 'pg', 'r', 'x', Gravatar::RATING_R); + foreach ($ratingsRight as $value) { + $this->helper->setRating($value); + $this->assertEquals($value, $this->helper->getRating()); + } + } + + public function testSetSecure() + { + $values = array("true", "false", "text", $this->view, 100, true, "", null, 0, false); + foreach ($values as $value) { + $this->helper->setSecure($value); + $this->assertInternalType('bool', $this->helper->getSecure()); + } + } + + /** + * Test SSL location + */ + public function testHttpsSource() + { + $this->assertRegExp( + '#src="https://secure.gravatar.com/avatar/[a-z0-9]{32}.+"#', + $this->helper->__invoke("example@example.com", array('secure' => true))->__toString() + ); + } + + /** + * Test HTML attribs + */ + public function testImgAttribs() + { + $this->assertRegExp( + '/class="gravatar" title="Gravatar"/', + $this->helper->__invoke("example@example.com", array(), array('class' => 'gravatar', 'title' => 'Gravatar'))->__toString() + ); + } + + /** + * Test gravatar's options (rating, size, default image and secure) + */ + public function testGravatarOptions() + { + $this->assertRegExp( + '#src="http://www.gravatar.com/avatar/[a-z0-9]{32}\?s=125&d=wavatar&r=pg"#', + $this->helper->__invoke("example@example.com", array('rating' => 'pg', 'imgSize' => 125, 'defaultImg' => 'wavatar', 'secure' => false))->__toString() + ); + } + + /** + * Test auto detect location. + * If request was made through the HTTPS protocol use secure location. + */ + public function testAutoDetectLocation() + { + $values = array("on", "", 1, true); + + foreach ($values as $value) { + $_SERVER['HTTPS'] = $value; + $this->assertRegExp( + '#src="https://secure.gravatar.com/avatar/[a-z0-9]{32}.+"#', + $this->helper->__invoke("example@example.com")->__toString() + ); + } + } + + /** + * @link http://php.net/manual/en/reserved.variables.server.php Section "HTTPS" + */ + public function testAutoDetectLocationOnIis() + { + $_SERVER['HTTPS'] = "off"; + + $this->assertRegExp( + '/src="http:\/\/www.gravatar.com\/avatar\/[a-z0-9]{32}.+"/', + $this->helper->__invoke("example@example.com")->__toString() + ); + } + + public function testSetAttribsWithSrcKey() + { + $email = 'example@example.com'; + $this->helper->setEmail($email); + $this->helper->setAttribs(array( + 'class' => 'gravatar', + 'src' => 'http://example.com', + 'id' => 'gravatarID', + )); + + $this->assertRegExp( + '#src="http://www.gravatar.com/avatar/[a-z0-9]{32}.+"#', + $this->helper->getImgTag() + ); + } + + public function testForgottenEmailParameter() + { + $this->assertRegExp( + '#(src="http://www.gravatar.com/avatar/[a-z0-9]{32}.+")#', + $this->helper->getImgTag() + ); + } + + public function testReturnImgTag() + { + $this->assertRegExp( + "/^helper->__invoke("example@example.com")->__toString() + ); + } + + public function testReturnThisObject() + { + $this->assertInstanceOf('Zend\View\Helper\Gravatar', $this->helper->__invoke()); + } + + public function testInvalidKeyPassedToSetOptionsMethod() + { + $options = array( + 'unknown' => array('val' => 1) + ); + $this->helper->__invoke()->setOptions($options); + } +} diff --git a/test/Helper/HeadLinkTest.php b/test/Helper/HeadLinkTest.php new file mode 100644 index 00000000..7882393b --- /dev/null +++ b/test/Helper/HeadLinkTest.php @@ -0,0 +1,413 @@ +basePath = __DIR__ . '/_files/modules'; + $this->view = new View(); + $this->helper = new Helper\HeadLink(); + $this->helper->setView($this->view); + } + + /** + * Tears down the fixture, for example, close a network connection. + * This method is called after a test is executed. + * + * @return void + */ + public function tearDown() + { + unset($this->helper); + } + + public function testNamespaceRegisteredInPlaceholderRegistryAfterInstantiation() + { + $registry = PlaceholderRegistry::getRegistry(); + if ($registry->containerExists('Zend_View_Helper_HeadLink')) { + $registry->deleteContainer('Zend_View_Helper_HeadLink'); + } + $this->assertFalse($registry->containerExists('Zend_View_Helper_HeadLink')); + $helper = new Helper\HeadLink(); + $this->assertTrue($registry->containerExists('Zend_View_Helper_HeadLink')); + } + + public function testHeadLinkReturnsObjectInstance() + { + $placeholder = $this->helper->__invoke(); + $this->assertTrue($placeholder instanceof Helper\HeadLink); + } + + public function testPrependThrowsExceptionWithoutArrayArgument() + { + $this->setExpectedException('Zend\View\Exception\ExceptionInterface'); + $this->helper->prepend('foo'); + } + + public function testAppendThrowsExceptionWithoutArrayArgument() + { + $this->setExpectedException('Zend\View\Exception\ExceptionInterface'); + $this->helper->append('foo'); + } + + public function testSetThrowsExceptionWithoutArrayArgument() + { + $this->setExpectedException('Zend\View\Exception\ExceptionInterface'); + $this->helper->set('foo'); + } + + public function testOffsetSetThrowsExceptionWithoutArrayArgument() + { + $this->setExpectedException('Zend\View\Exception\ExceptionInterface'); + $this->helper->offsetSet(1, 'foo'); + } + + public function testCreatingLinkStackViaHeadScriptCreatesAppropriateOutput() + { + $links = array( + 'link1' => array('rel' => 'stylesheet', 'type' => 'text/css', 'href' => 'foo'), + 'link2' => array('rel' => 'stylesheet', 'type' => 'text/css', 'href' => 'bar'), + 'link3' => array('rel' => 'stylesheet', 'type' => 'text/css', 'href' => 'baz'), + ); + $this->helper->__invoke($links['link1']) + ->__invoke($links['link2'], 'PREPEND') + ->__invoke($links['link3']); + + $string = $this->helper->toString(); + $lines = substr_count($string, PHP_EOL); + $this->assertEquals(2, $lines); + $lines = substr_count($string, 'assertEquals(3, $lines, $string); + + foreach ($links as $link) { + $substr = ' href="' . $link['href'] . '"'; + $this->assertContains($substr, $string); + $substr = ' rel="' . $link['rel'] . '"'; + $this->assertContains($substr, $string); + $substr = ' type="' . $link['type'] . '"'; + $this->assertContains($substr, $string); + } + + $order = array(); + foreach ($this->helper as $key => $value) { + if (isset($value->href)) { + $order[$key] = $value->href; + } + } + $expected = array('bar', 'foo', 'baz'); + $this->assertSame($expected, $order); + } + + public function testCreatingLinkStackViaStyleSheetMethodsCreatesAppropriateOutput() + { + $links = array( + 'link1' => array('rel' => 'stylesheet', 'type' => 'text/css', 'href' => 'foo'), + 'link2' => array('rel' => 'stylesheet', 'type' => 'text/css', 'href' => 'bar'), + 'link3' => array('rel' => 'stylesheet', 'type' => 'text/css', 'href' => 'baz'), + ); + $this->helper->appendStylesheet($links['link1']['href']) + ->prependStylesheet($links['link2']['href']) + ->appendStylesheet($links['link3']['href']); + + $string = $this->helper->toString(); + $lines = substr_count($string, PHP_EOL); + $this->assertEquals(2, $lines); + $lines = substr_count($string, 'assertEquals(3, $lines, $string); + + foreach ($links as $link) { + $substr = ' href="' . $link['href'] . '"'; + $this->assertContains($substr, $string); + $substr = ' rel="' . $link['rel'] . '"'; + $this->assertContains($substr, $string); + $substr = ' type="' . $link['type'] . '"'; + $this->assertContains($substr, $string); + } + + $order = array(); + foreach ($this->helper as $key => $value) { + if (isset($value->href)) { + $order[$key] = $value->href; + } + } + $expected = array('bar', 'foo', 'baz'); + $this->assertSame($expected, $order); + } + + public function testCreatingLinkStackViaAlternateMethodsCreatesAppropriateOutput() + { + $links = array( + 'link1' => array('title' => 'stylesheet', 'type' => 'text/css', 'href' => 'foo'), + 'link2' => array('title' => 'stylesheet', 'type' => 'text/css', 'href' => 'bar'), + 'link3' => array('title' => 'stylesheet', 'type' => 'text/css', 'href' => 'baz'), + ); + $where = 'append'; + foreach ($links as $link) { + $method = $where . 'Alternate'; + $this->helper->$method($link['href'], $link['type'], $link['title']); + $where = ('append' == $where) ? 'prepend' : 'append'; + } + + $string = $this->helper->toString(); + $lines = substr_count($string, PHP_EOL); + $this->assertEquals(2, $lines); + $lines = substr_count($string, 'assertEquals(3, $lines, $string); + $lines = substr_count($string, ' rel="alternate"'); + $this->assertEquals(3, $lines, $string); + + foreach ($links as $link) { + $substr = ' href="' . $link['href'] . '"'; + $this->assertContains($substr, $string); + $substr = ' title="' . $link['title'] . '"'; + $this->assertContains($substr, $string); + $substr = ' type="' . $link['type'] . '"'; + $this->assertContains($substr, $string); + } + + $order = array(); + foreach ($this->helper as $key => $value) { + if (isset($value->href)) { + $order[$key] = $value->href; + } + } + $expected = array('bar', 'foo', 'baz'); + $this->assertSame($expected, $order); + } + + public function testOverloadingThrowsExceptionWithNoArguments() + { + $this->setExpectedException('Zend\View\Exception\ExceptionInterface'); + $this->helper->appendStylesheet(); + } + + public function testOverloadingShouldAllowSingleArrayArgument() + { + $this->helper->setStylesheet(array('href' => '/styles.css')); + $link = $this->helper->getValue(); + $this->assertEquals('/styles.css', $link->href); + } + + public function testOverloadingUsingSingleArrayArgumentWithInvalidValuesThrowsException() + { + $this->setExpectedException('Zend\View\Exception\ExceptionInterface'); + $this->helper->setStylesheet(array('bogus' => 'unused')); + } + + public function testOverloadingOffsetSetWorks() + { + $this->helper->offsetSetStylesheet(100, '/styles.css'); + $items = $this->helper->getArrayCopy(); + $this->assertTrue(isset($items[100])); + $link = $items[100]; + $this->assertEquals('/styles.css', $link->href); + } + + public function testOverloadingThrowsExceptionWithInvalidMethod() + { + $this->setExpectedException('Zend\View\Exception\ExceptionInterface'); + $this->helper->bogusMethod(); + } + + public function testStylesheetAttributesGetSet() + { + $this->helper->setStylesheet('/styles.css', 'projection', 'ie6'); + $item = $this->helper->getValue(); + $this->assertObjectHasAttribute('media', $item); + $this->assertObjectHasAttribute('conditionalStylesheet', $item); + + $this->assertEquals('projection', $item->media); + $this->assertEquals('ie6', $item->conditionalStylesheet); + } + + public function testConditionalStylesheetNotCreatedByDefault() + { + $this->helper->setStylesheet('/styles.css'); + $item = $this->helper->getValue(); + $this->assertObjectHasAttribute('conditionalStylesheet', $item); + $this->assertFalse($item->conditionalStylesheet); + + $string = $this->helper->toString(); + $this->assertContains('/styles.css', $string); + $this->assertNotContains('', $string); + } + + public function testConditionalStylesheetCreationOccursWhenRequested() + { + $this->helper->setStylesheet('/styles.css', 'screen', 'ie6'); + $item = $this->helper->getValue(); + $this->assertObjectHasAttribute('conditionalStylesheet', $item); + $this->assertEquals('ie6', $item->conditionalStylesheet); + + $string = $this->helper->toString(); + $this->assertContains('/styles.css', $string); + $this->assertContains('', $string); + } + + public function testSettingAlternateWithTooFewArgsRaisesException() + { + try { + $this->helper->setAlternate('foo'); + $this->fail('Setting alternate with fewer than 3 args should raise exception'); + } catch (ViewException $e) { } + try { + $this->helper->setAlternate('foo', 'bar'); + $this->fail('Setting alternate with fewer than 3 args should raise exception'); + } catch (ViewException $e) { } + } + + public function testIndentationIsHonored() + { + $this->helper->setIndent(4); + $this->helper->appendStylesheet('/css/screen.css'); + $this->helper->appendStylesheet('/css/rules.css'); + $string = $this->helper->toString(); + + $scripts = substr_count($string, ' assertEquals(2, $scripts); + } + + public function testLinkRendersAsPlainHtmlIfDoctypeNotXhtml() + { + $this->view->plugin('doctype')->__invoke('HTML4_STRICT'); + $this->helper->__invoke(array('rel' => 'icon', 'src' => '/foo/bar')) + ->__invoke(array('rel' => 'foo', 'href' => '/bar/baz')); + $test = $this->helper->toString(); + $this->assertNotContains(' />', $test); + } + + public function testDoesNotAllowDuplicateStylesheets() + { + $this->helper->appendStylesheet('foo'); + $this->helper->appendStylesheet('foo'); + $this->assertEquals(1, count($this->helper), var_export($this->helper->getContainer()->getArrayCopy(), 1)); + } + + /** + * test for ZF-2889 + */ + public function testBooleanStylesheet() + { + $this->helper->appendStylesheet(array('href' => '/bar/baz', 'conditionalStylesheet' => false)); + $test = $this->helper->toString(); + $this->assertNotContains('[if false]', $test); + } + + /** + * test for ZF-3271 + * + */ + public function testBooleanTrueConditionalStylesheet() + { + $this->helper->appendStylesheet(array('href' => '/bar/baz', 'conditionalStylesheet' => true)); + $test = $this->helper->toString(); + $this->assertNotContains('[if 1]', $test); + $this->assertNotContains('[if true]', $test); + } + + /** + * @issue ZF-3928 + * @link http://framework.zend.com/issues/browse/ZF-3928 + */ + public function testTurnOffAutoEscapeDoesNotEncodeAmpersand() + { + $this->helper->setAutoEscape(false)->appendStylesheet('/css/rules.css?id=123&foo=bar'); + $this->assertContains('id=123&foo=bar', $this->helper->toString()); + } + + public function testSetAlternateWithExtras() + { + $this->helper->setAlternate('/mydocument.pdf', 'application/pdf', 'foo', array('media' => array('print','screen'))); + $test = $this->helper->toString(); + $this->assertContains('media="print,screen"', $test); + } + + public function testAppendStylesheetWithExtras() + { + $this->helper->appendStylesheet(array('href' => '/bar/baz', 'conditionalStylesheet' => false, 'extras' => array('id' => 'my_link_tag'))); + $test = $this->helper->toString(); + $this->assertContains('id="my_link_tag"', $test); + } + + public function testSetStylesheetWithMediaAsArray() + { + $this->helper->appendStylesheet('/bar/baz', array('screen','print')); + $test = $this->helper->toString(); + $this->assertContains(' media="screen,print"', $test); + } + + /** + * @issue ZF-5435 + */ + public function testContainerMaintainsCorrectOrderOfItems() + { + $this->helper->__invoke()->offsetSetStylesheet(1,'/test1.css'); + $this->helper->__invoke()->offsetSetStylesheet(10,'/test2.css'); + $this->helper->__invoke()->offsetSetStylesheet(20,'/test3.css'); + $this->helper->__invoke()->offsetSetStylesheet(5,'/test4.css'); + + $test = $this->helper->toString(); + + $expected = '' . PHP_EOL + . '' . PHP_EOL + . '' . PHP_EOL + . ''; + + $this->assertEquals($expected, $test); + } + + /** + * @issue ZF-10345 + */ + public function testIdAttributeIsSupported() + { + $this->helper->appendStylesheet(array('href' => '/bar/baz', 'id' => 'foo')); + $this->assertContains('id="foo"', $this->helper->toString()); + } +} + diff --git a/test/Helper/HeadMetaTest.php b/test/Helper/HeadMetaTest.php new file mode 100644 index 00000000..91f00bb0 --- /dev/null +++ b/test/Helper/HeadMetaTest.php @@ -0,0 +1,491 @@ +error = false; + PlaceholderRegistry::unsetRegistry(); + Helper\Doctype::unsetDoctypeRegistry(); + $this->basePath = __DIR__ . '/_files/modules'; + $this->view = new View(); + $this->view->plugin('doctype')->__invoke('XHTML1_STRICT'); + $this->helper = new Helper\HeadMeta(); + $this->helper->setView($this->view); + } + + /** + * Tears down the fixture, for example, close a network connection. + * This method is called after a test is executed. + * + * @return void + */ + public function tearDown() + { + unset($this->helper); + } + + public function handleErrors($errno, $errstr) + { + $this->error = $errstr; + } + + public function testNamespaceRegisteredInPlaceholderRegistryAfterInstantiation() + { + $registry = PlaceholderRegistry::getRegistry(); + if ($registry->containerExists('Zend_View_Helper_HeadMeta')) { + $registry->deleteContainer('Zend_View_Helper_HeadMeta'); + } + $this->assertFalse($registry->containerExists('Zend_View_Helper_HeadMeta')); + $helper = new Helper\HeadMeta(); + $this->assertTrue($registry->containerExists('Zend_View_Helper_HeadMeta')); + } + + public function testHeadMetaReturnsObjectInstance() + { + $placeholder = $this->helper->__invoke(); + $this->assertTrue($placeholder instanceof Helper\HeadMeta); + } + + public function testAppendPrependAndSetThrowExceptionsWhenNonMetaValueProvided() + { + try { + $this->helper->append('foo'); + $this->fail('Non-meta value should not append'); + } catch (ViewException $e) { + } + try { + $this->helper->offsetSet(3, 'foo'); + $this->fail('Non-meta value should not offsetSet'); + } catch (ViewException $e) { + } + try { + $this->helper->prepend('foo'); + $this->fail('Non-meta value should not prepend'); + } catch (ViewException $e) { + } + try { + $this->helper->set('foo'); + $this->fail('Non-meta value should not set'); + } catch (ViewException $e) { + } + } + + protected function _inflectAction($type) + { + $type = str_replace('-', ' ', $type); + $type = ucwords($type); + $type = str_replace(' ', '', $type); + return $type; + } + + protected function _testOverloadAppend($type) + { + $action = 'append' . $this->_inflectAction($type); + $string = 'foo'; + for ($i = 0; $i < 3; ++$i) { + $string .= ' foo'; + $this->helper->$action('keywords', $string); + $values = $this->helper->getArrayCopy(); + $this->assertEquals($i + 1, count($values)); + + $item = $values[$i]; + $this->assertObjectHasAttribute('type', $item); + $this->assertObjectHasAttribute('modifiers', $item); + $this->assertObjectHasAttribute('content', $item); + $this->assertObjectHasAttribute($item->type, $item); + $this->assertEquals('keywords', $item->{$item->type}); + $this->assertEquals($string, $item->content); + } + } + + protected function _testOverloadPrepend($type) + { + $action = 'prepend' . $this->_inflectAction($type); + $string = 'foo'; + for ($i = 0; $i < 3; ++$i) { + $string .= ' foo'; + $this->helper->$action('keywords', $string); + $values = $this->helper->getArrayCopy(); + $this->assertEquals($i + 1, count($values)); + $item = array_shift($values); + + $this->assertObjectHasAttribute('type', $item); + $this->assertObjectHasAttribute('modifiers', $item); + $this->assertObjectHasAttribute('content', $item); + $this->assertObjectHasAttribute($item->type, $item); + $this->assertEquals('keywords', $item->{$item->type}); + $this->assertEquals($string, $item->content); + } + } + + protected function _testOverloadSet($type) + { + $setAction = 'set' . $this->_inflectAction($type); + $appendAction = 'append' . $this->_inflectAction($type); + $string = 'foo'; + for ($i = 0; $i < 3; ++$i) { + $this->helper->$appendAction('keywords', $string); + $string .= ' foo'; + } + $this->helper->$setAction('keywords', $string); + $values = $this->helper->getArrayCopy(); + $this->assertEquals(1, count($values)); + $item = array_shift($values); + + $this->assertObjectHasAttribute('type', $item); + $this->assertObjectHasAttribute('modifiers', $item); + $this->assertObjectHasAttribute('content', $item); + $this->assertObjectHasAttribute($item->type, $item); + $this->assertEquals('keywords', $item->{$item->type}); + $this->assertEquals($string, $item->content); + } + + public function testOverloadingAppendNameAppendsMetaTagToStack() + { + $this->_testOverloadAppend('name'); + } + + public function testOverloadingPrependNamePrependsMetaTagToStack() + { + $this->_testOverloadPrepend('name'); + } + + public function testOverloadingSetNameOverwritesMetaTagStack() + { + $this->_testOverloadSet('name'); + } + + public function testOverloadingAppendHttpEquivAppendsMetaTagToStack() + { + $this->_testOverloadAppend('http-equiv'); + } + + public function testOverloadingPrependHttpEquivPrependsMetaTagToStack() + { + $this->_testOverloadPrepend('http-equiv'); + } + + public function testOverloadingSetHttpEquivOverwritesMetaTagStack() + { + $this->_testOverloadSet('http-equiv'); + } + + public function testOverloadingThrowsExceptionWithFewerThanTwoArgs() + { + $this->setExpectedException('Zend\View\Exception\ExceptionInterface'); + $this->helper->setName('foo'); + } + + public function testOverloadingThrowsExceptionWithInvalidMethodType() + { + $this->setExpectedException('Zend\View\Exception\ExceptionInterface'); + $this->helper->setFoo('foo'); + } + + public function testCanBuildMetaTagsWithAttributes() + { + $this->helper->setName('keywords', 'foo bar', array('lang' => 'us_en', 'scheme' => 'foo', 'bogus' => 'unused')); + $value = $this->helper->getValue(); + + $this->assertObjectHasAttribute('modifiers', $value); + $modifiers = $value->modifiers; + $this->assertTrue(array_key_exists('lang', $modifiers)); + $this->assertEquals('us_en', $modifiers['lang']); + $this->assertTrue(array_key_exists('scheme', $modifiers)); + $this->assertEquals('foo', $modifiers['scheme']); + } + + public function testToStringReturnsValidHtml() + { + $this->helper->setName('keywords', 'foo bar', array('lang' => 'us_en', 'scheme' => 'foo', 'bogus' => 'unused')) + ->prependName('title', 'boo bah') + ->appendHttpEquiv('screen', 'projection'); + $string = $this->helper->toString(); + + $metas = substr_count($string, 'assertEquals(3, $metas); + $metas = substr_count($string, '/>'); + $this->assertEquals(3, $metas); + $metas = substr_count($string, 'name="'); + $this->assertEquals(2, $metas); + $metas = substr_count($string, 'http-equiv="'); + $this->assertEquals(1, $metas); + + $this->assertContains('http-equiv="screen" content="projection"', $string); + $this->assertContains('name="keywords" content="foo bar"', $string); + $this->assertContains('lang="us_en"', $string); + $this->assertContains('scheme="foo"', $string); + $this->assertNotContains('bogus', $string); + $this->assertNotContains('unused', $string); + $this->assertContains('name="title" content="boo bah"', $string); + } + + /** + * @group ZF-6637 + */ + public function testToStringWhenInvalidKeyProvidedShouldConvertThrownException() + { + $this->helper->__invoke('some-content', 'tag value', 'not allowed key'); + set_error_handler(array($this, 'handleErrors')); + $string = @$this->helper->toString(); + $this->assertEquals('', $string); + $this->assertTrue(is_string($this->error)); + } + + public function testHeadMetaHelperCreatesItemEntry() + { + $this->helper->__invoke('foo', 'keywords'); + $values = $this->helper->getArrayCopy(); + $this->assertEquals(1, count($values)); + $item = array_shift($values); + $this->assertEquals('foo', $item->content); + $this->assertEquals('name', $item->type); + $this->assertEquals('keywords', $item->name); + } + + public function testOverloadingOffsetInsertsAtOffset() + { + $this->helper->offsetSetName(100, 'keywords', 'foo'); + $values = $this->helper->getArrayCopy(); + $this->assertEquals(1, count($values)); + $this->assertTrue(array_key_exists(100, $values)); + $item = $values[100]; + $this->assertEquals('foo', $item->content); + $this->assertEquals('name', $item->type); + $this->assertEquals('keywords', $item->name); + } + + public function testIndentationIsHonored() + { + $this->helper->setIndent(4); + $this->helper->appendName('keywords', 'foo bar'); + $this->helper->appendName('seo', 'baz bat'); + $string = $this->helper->toString(); + + $scripts = substr_count($string, ' __invoke('HTML4_STRICT'); + $this->helper->__invoke('some content', 'foo'); + $test = $this->helper->toString(); + $this->assertNotContains('/>', $test); + $this->assertContains('some content', $test); + $this->assertContains('foo', $test); + } + + /** + * @issue ZF-2663 + */ + public function testSetNameDoesntClobber() + { + $view = new View(); + $view->plugin('headMeta')->setName('keywords', 'foo'); + $view->plugin('headMeta')->appendHttpEquiv('pragma', 'bar'); + $view->plugin('headMeta')->appendHttpEquiv('Cache-control', 'baz'); + $view->plugin('headMeta')->setName('keywords', 'bat'); + + $this->assertEquals( + '' . PHP_EOL . '' . PHP_EOL . '', + $view->plugin('headMeta')->toString() + ); + } + + /** + * @issue ZF-2663 + */ + public function testSetNameDoesntClobberPart2() + { + $view = new View(); + $view->plugin('headMeta')->setName('keywords', 'foo'); + $view->plugin('headMeta')->setName('description', 'foo'); + $view->plugin('headMeta')->appendHttpEquiv('pragma', 'baz'); + $view->plugin('headMeta')->appendHttpEquiv('Cache-control', 'baz'); + $view->plugin('headMeta')->setName('keywords', 'bar'); + + $this->assertEquals( + '' . PHP_EOL . '' . PHP_EOL . '' . PHP_EOL . '', + $view->plugin('headMeta')->toString() + ); + } + + /** + * @issue ZF-3780 + * @link http://framework.zend.com/issues/browse/ZF-3780 + */ + public function testPlacesMetaTagsInProperOrder() + { + $view = new View(); + $view->plugin('headMeta')->setName('keywords', 'foo'); + $view->plugin('headMeta')->__invoke('some content', 'bar', 'name', array(), \Zend\View\Helper\Placeholder\Container\AbstractContainer::PREPEND); + + $this->assertEquals( + '' . PHP_EOL . '', + $view->plugin('headMeta')->toString() + ); + } + + /** + * @issue ZF-5435 + */ + public function testContainerMaintainsCorrectOrderOfItems() + { + + $this->helper->offsetSetName(1, 'keywords', 'foo'); + $this->helper->offsetSetName(10, 'description', 'foo'); + $this->helper->offsetSetHttpEquiv(20, 'pragma', 'baz'); + $this->helper->offsetSetHttpEquiv(5, 'Cache-control', 'baz'); + + $test = $this->helper->toString(); + + $expected = '' . PHP_EOL + . '' . PHP_EOL + . '' . PHP_EOL + . ''; + + $this->assertEquals($expected, $test); + } + + /** + * @issue ZF-7722 + */ + public function testCharsetValidateFail() + { + $view = new View(); + $view->plugin('doctype')->__invoke('HTML4_STRICT'); + + $this->setExpectedException('Zend\View\Exception\ExceptionInterface'); + $view->plugin('headMeta')->setCharset('utf-8'); + } + + /** + * @issue ZF-7722 + */ + public function testCharset() + { + $view = new View(); + $view->plugin('doctype')->__invoke('HTML5'); + + $view->plugin('headMeta')->setCharset('utf-8'); + $this->assertEquals( + '', + $view->plugin('headMeta')->toString()); + + $view->plugin('doctype')->__invoke('XHTML5'); + + $this->assertEquals( + '', + $view->plugin('headMeta')->toString()); + } + + /** + * @group ZF-9743 + */ + public function testPropertyIsSupportedWithRdfaDoctype() + { + $this->view->doctype('XHTML1_RDFA'); + $this->helper->__invoke('foo', 'og:title', 'property'); + $this->assertEquals('', + $this->helper->toString() + ); + } + + /** + * @group ZF-9743 + */ + public function testPropertyIsNotSupportedByDefaultDoctype() + { + try { + $this->helper->__invoke('foo', 'og:title', 'property'); + $this->fail('meta property attribute should not be supported on default doctype'); + } catch (ViewException $e) { + $this->assertContains('Invalid value passed', $e->getMessage()); + } + } + + /** + * @group ZF-9743 + * @depends testPropertyIsSupportedWithRdfaDoctype + */ + public function testOverloadingAppendPropertyAppendsMetaTagToStack() + { + $this->view->doctype('XHTML1_RDFA'); + $this->_testOverloadAppend('property'); + } + + /** + * @group ZF-9743 + * @depends testPropertyIsSupportedWithRdfaDoctype + */ + public function testOverloadingPrependPropertyPrependsMetaTagToStack() + { + $this->view->doctype('XHTML1_RDFA'); + $this->_testOverloadPrepend('property'); + } + + /** + * @group ZF-9743 + * @depends testPropertyIsSupportedWithRdfaDoctype + */ + public function testOverloadingSetPropertyOverwritesMetaTagStack() + { + $this->view->doctype('XHTML1_RDFA'); + $this->_testOverloadSet('property'); + } + + /** + * @group ZF-11835 + */ + public function testConditional() + { + $html = $this->helper->appendHttpEquiv('foo', 'bar', array('conditional' => 'lt IE 7'))->toString(); + + $this->assertRegExp("|^$|", $html); + } + +} + diff --git a/test/Helper/HeadScriptTest.php b/test/Helper/HeadScriptTest.php new file mode 100644 index 00000000..efa78428 --- /dev/null +++ b/test/Helper/HeadScriptTest.php @@ -0,0 +1,452 @@ +basePath = __DIR__ . '/_files/modules'; + $this->helper = new Helper\HeadScript(); + } + + /** + * Tears down the fixture, for example, close a network connection. + * This method is called after a test is executed. + * + * @return void + */ + public function tearDown() + { + unset($this->helper); + } + + public function testNamespaceRegisteredInPlaceholderRegistryAfterInstantiation() + { + $registry = Registry::getRegistry(); + if ($registry->containerExists('Zend_View_Helper_HeadScript')) { + $registry->deleteContainer('Zend_View_Helper_HeadScript'); + } + $this->assertFalse($registry->containerExists('Zend_View_Helper_HeadScript')); + $helper = new Helper\HeadScript(); + $this->assertTrue($registry->containerExists('Zend_View_Helper_HeadScript')); + } + + public function testHeadScriptReturnsObjectInstance() + { + $placeholder = $this->helper->__invoke(); + $this->assertTrue($placeholder instanceof Helper\HeadScript); + } + + public function testSetPrependAppendAndOffsetSetThrowExceptionsOnInvalidItems() + { + try { + $this->helper->append('foo'); + $this->fail('Append should throw exception with invalid item'); + } catch (View\Exception\ExceptionInterface $e) { } + try { + $this->helper->offsetSet(1, 'foo'); + $this->fail('OffsetSet should throw exception with invalid item'); + } catch (View\Exception\ExceptionInterface $e) { } + try { + $this->helper->prepend('foo'); + $this->fail('Prepend should throw exception with invalid item'); + } catch (View\Exception\ExceptionInterface $e) { } + try { + $this->helper->set('foo'); + $this->fail('Set should throw exception with invalid item'); + } catch (View\Exception\ExceptionInterface $e) { } + } + + protected function _inflectAction($type) + { + return ucfirst(strtolower($type)); + } + + protected function _testOverloadAppend($type) + { + $action = 'append' . $this->_inflectAction($type); + $string = 'foo'; + for ($i = 0; $i < 3; ++$i) { + $string .= ' foo'; + $this->helper->$action($string); + $values = $this->helper->getArrayCopy(); + $this->assertEquals($i + 1, count($values)); + if ('file' == $type) { + $this->assertEquals($string, $values[$i]->attributes['src']); + } elseif ('script' == $type) { + $this->assertEquals($string, $values[$i]->source); + } + $this->assertEquals('text/javascript', $values[$i]->type); + } + } + + protected function _testOverloadPrepend($type) + { + $action = 'prepend' . $this->_inflectAction($type); + $string = 'foo'; + for ($i = 0; $i < 3; ++$i) { + $string .= ' foo'; + $this->helper->$action($string); + $values = $this->helper->getArrayCopy(); + $this->assertEquals($i + 1, count($values)); + $first = array_shift($values); + if ('file' == $type) { + $this->assertEquals($string, $first->attributes['src']); + } elseif ('script' == $type) { + $this->assertEquals($string, $first->source); + } + $this->assertEquals('text/javascript', $first->type); + } + } + + protected function _testOverloadSet($type) + { + $action = 'set' . $this->_inflectAction($type); + $string = 'foo'; + for ($i = 0; $i < 3; ++$i) { + $this->helper->appendScript($string); + $string .= ' foo'; + } + $this->helper->$action($string); + $values = $this->helper->getArrayCopy(); + $this->assertEquals(1, count($values)); + if ('file' == $type) { + $this->assertEquals($string, $values[0]->attributes['src']); + } elseif ('script' == $type) { + $this->assertEquals($string, $values[0]->source); + } + $this->assertEquals('text/javascript', $values[0]->type); + } + + protected function _testOverloadOffsetSet($type) + { + $action = 'offsetSet' . $this->_inflectAction($type); + $string = 'foo'; + $this->helper->$action(5, $string); + $values = $this->helper->getArrayCopy(); + $this->assertEquals(1, count($values)); + if ('file' == $type) { + $this->assertEquals($string, $values[5]->attributes['src']); + } elseif ('script' == $type) { + $this->assertEquals($string, $values[5]->source); + } + $this->assertEquals('text/javascript', $values[5]->type); + } + + public function testOverloadAppendFileAppendsScriptsToStack() + { + $this->_testOverloadAppend('file'); + } + + public function testOverloadAppendScriptAppendsScriptsToStack() + { + $this->_testOverloadAppend('script'); + } + + public function testOverloadPrependFileAppendsScriptsToStack() + { + $this->_testOverloadPrepend('file'); + } + + public function testOverloadPrependScriptAppendsScriptsToStack() + { + $this->_testOverloadPrepend('script'); + } + + public function testOverloadSetFileOverwritesStack() + { + $this->_testOverloadSet('file'); + } + + public function testOverloadSetScriptOverwritesStack() + { + $this->_testOverloadSet('script'); + } + + public function testOverloadOffsetSetFileWritesToSpecifiedIndex() + { + $this->_testOverloadOffsetSet('file'); + } + + public function testOverloadOffsetSetScriptWritesToSpecifiedIndex() + { + $this->_testOverloadOffsetSet('script'); + } + + public function testOverloadingThrowsExceptionWithInvalidMethod() + { + try { + $this->helper->fooBar('foo'); + $this->fail('Invalid method should raise exception'); + } catch (View\Exception\ExceptionInterface $e) { + } + } + + public function testOverloadingWithTooFewArgumentsRaisesException() + { + try { + $this->helper->setScript(); + $this->fail('Too few arguments should raise exception'); + } catch (View\Exception\ExceptionInterface $e) { + } + + try { + $this->helper->offsetSetScript(5); + $this->fail('Too few arguments should raise exception'); + } catch (View\Exception\ExceptionInterface $e) { + } + } + + public function testHeadScriptAppropriatelySetsScriptItems() + { + $this->helper->__invoke('FILE', 'foo', 'set') + ->__invoke('SCRIPT', 'bar', 'prepend') + ->__invoke('SCRIPT', 'baz', 'append'); + $items = $this->helper->getArrayCopy(); + for ($i = 0; $i < 3; ++$i) { + $item = $items[$i]; + switch ($i) { + case 0: + $this->assertObjectHasAttribute('source', $item); + $this->assertEquals('bar', $item->source); + break; + case 1: + $this->assertObjectHasAttribute('attributes', $item); + $this->assertTrue(isset($item->attributes['src'])); + $this->assertEquals('foo', $item->attributes['src']); + break; + case 2: + $this->assertObjectHasAttribute('source', $item); + $this->assertEquals('baz', $item->source); + break; + } + } + } + + public function testToStringRendersValidHtml() + { + $this->helper->__invoke('FILE', 'foo', 'set') + ->__invoke('SCRIPT', 'bar', 'prepend') + ->__invoke('SCRIPT', 'baz', 'append'); + $string = $this->helper->toString(); + + $scripts = substr_count($string, ''); + $this->assertEquals(3, $scripts); + $scripts = substr_count($string, 'src="'); + $this->assertEquals(1, $scripts); + $scripts = substr_count($string, '><'); + $this->assertEquals(1, $scripts); + + $this->assertContains('src="foo"', $string); + $this->assertContains('bar', $string); + $this->assertContains('baz', $string); + + $doc = new \DOMDocument; + $dom = $doc->loadHtml($string); + $this->assertTrue($dom !== false); + } + + public function testCapturingCapturesToObject() + { + $this->helper->captureStart(); + echo 'foobar'; + $this->helper->captureEnd(); + $values = $this->helper->getArrayCopy(); + $this->assertEquals(1, count($values), var_export($values, 1)); + $item = array_shift($values); + $this->assertContains('foobar', $item->source); + } + + public function testIndentationIsHonored() + { + $this->helper->setIndent(4); + $this->helper->appendScript(' +var foo = "bar"; + document.write(foo.strlen());'); + $this->helper->appendScript(' +var bar = "baz"; +document.write(bar.strlen());'); + $string = $this->helper->toString(); + + $scripts = substr_count($string, ' assertEquals(2, $scripts); + $this->assertContains(' //', $string); + $this->assertContains('var', $string); + $this->assertContains('document', $string); + $this->assertContains(' document', $string); + } + + public function testDoesNotAllowDuplicateFiles() + { + $this->helper->__invoke('FILE', '/js/prototype.js'); + $this->helper->__invoke('FILE', '/js/prototype.js'); + $this->assertEquals(1, count($this->helper)); + } + + public function testRenderingDoesNotRenderArbitraryAttributesByDefault() + { + $this->helper->__invoke()->appendFile('/js/foo.js', 'text/javascript', array('bogus' => 'deferred')); + $test = $this->helper->__invoke()->toString(); + $this->assertNotContains('bogus="deferred"', $test); + } + + public function testCanRenderArbitraryAttributesOnRequest() + { + $this->helper->__invoke()->appendFile('/js/foo.js', 'text/javascript', array('bogus' => 'deferred')) + ->setAllowArbitraryAttributes(true); + $test = $this->helper->__invoke()->toString(); + $this->assertContains('bogus="deferred"', $test); + } + + public function testCanPerformMultipleSerialCaptures() + { + $this->helper->__invoke()->captureStart(); + echo "this is something captured"; + $this->helper->__invoke()->captureEnd(); + + $this->helper->__invoke()->captureStart(); + + echo "this is something else captured"; + $this->helper->__invoke()->captureEnd(); + } + + public function testCannotNestCaptures() + { + $this->helper->__invoke()->captureStart(); + echo "this is something captured"; + try { + $this->helper->__invoke()->captureStart(); + $this->helper->__invoke()->captureEnd(); + $this->fail('Should not be able to nest captures'); + } catch (View\Exception\ExceptionInterface $e) { + $this->helper->__invoke()->captureEnd(); + $this->assertContains('Cannot nest', $e->getMessage()); + } + } + + /** + * @issue ZF-3928 + * @link http://framework.zend.com/issues/browse/ZF-3928 + */ + public function testTurnOffAutoEscapeDoesNotEncodeAmpersand() + { + $this->helper->setAutoEscape(false)->appendFile('test.js?id=123&foo=bar'); + $this->assertEquals('', $this->helper->toString()); + } + + public function testConditionalScript() + { + $this->helper->__invoke()->appendFile('/js/foo.js', 'text/javascript', array('conditional' => 'lt IE 7')); + $test = $this->helper->__invoke()->toString(); + $this->assertContains('', $test); + } + + public function testNoEscapeTrue() + { + $this->helper->__invoke()->appendScript('// some script' . PHP_EOL, 'text/javascript', array('noescape' => true)); + $test = $this->helper->__invoke()->toString(); + + $this->assertNotContains('//', $test); + } + +} diff --git a/test/Helper/HeadStyleTest.php b/test/Helper/HeadStyleTest.php new file mode 100644 index 00000000..77f606f7 --- /dev/null +++ b/test/Helper/HeadStyleTest.php @@ -0,0 +1,418 @@ +basePath = __DIR__ . '/_files/modules'; + $this->helper = new Helper\HeadStyle(); + } + + /** + * Tears down the fixture, for example, close a network connection. + * This method is called after a test is executed. + * + * @return void + */ + public function tearDown() + { + unset($this->helper); + } + + public function testNamespaceRegisteredInPlaceholderRegistryAfterInstantiation() + { + $registry = Registry::getRegistry(); + if ($registry->containerExists('Zend_View_Helper_HeadStyle')) { + $registry->deleteContainer('Zend_View_Helper_HeadStyle'); + } + $this->assertFalse($registry->containerExists('Zend_View_Helper_HeadStyle')); + $helper = new Helper\HeadStyle(); + $this->assertTrue($registry->containerExists('Zend_View_Helper_HeadStyle')); + } + + public function testHeadStyleReturnsObjectInstance() + { + $placeholder = $this->helper->__invoke(); + $this->assertTrue($placeholder instanceof Helper\HeadStyle); + } + + public function testAppendPrependAndSetThrowExceptionsWhenNonStyleValueProvided() + { + try { + $this->helper->append('foo'); + $this->fail('Non-style value should not append'); + } catch (View\Exception\ExceptionInterface $e) { } + try { + $this->helper->offsetSet(5, 'foo'); + $this->fail('Non-style value should not offsetSet'); + } catch (View\Exception\ExceptionInterface $e) { } + try { + $this->helper->prepend('foo'); + $this->fail('Non-style value should not prepend'); + } catch (View\Exception\ExceptionInterface $e) { } + try { + $this->helper->set('foo'); + $this->fail('Non-style value should not set'); + } catch (View\Exception\ExceptionInterface $e) { } + } + + public function testOverloadAppendStyleAppendsStyleToStack() + { + $string = 'a {}'; + for ($i = 0; $i < 3; ++$i) { + $string .= PHP_EOL . 'a {}'; + $this->helper->appendStyle($string); + $values = $this->helper->getArrayCopy(); + $this->assertEquals($i + 1, count($values)); + $item = $values[$i]; + + $this->assertTrue($item instanceof \stdClass); + $this->assertObjectHasAttribute('content', $item); + $this->assertObjectHasAttribute('attributes', $item); + $this->assertEquals($string, $item->content); + } + } + + public function testOverloadPrependStylePrependsStyleToStack() + { + $string = 'a {}'; + for ($i = 0; $i < 3; ++$i) { + $string .= PHP_EOL . 'a {}'; + $this->helper->prependStyle($string); + $values = $this->helper->getArrayCopy(); + $this->assertEquals($i + 1, count($values)); + $item = array_shift($values); + + $this->assertTrue($item instanceof \stdClass); + $this->assertObjectHasAttribute('content', $item); + $this->assertObjectHasAttribute('attributes', $item); + $this->assertEquals($string, $item->content); + } + } + + public function testOverloadSetOversitesStack() + { + $string = 'a {}'; + for ($i = 0; $i < 3; ++$i) { + $this->helper->appendStyle($string); + $string .= PHP_EOL . 'a {}'; + } + $this->helper->setStyle($string); + $values = $this->helper->getArrayCopy(); + $this->assertEquals(1, count($values)); + $item = array_shift($values); + + $this->assertTrue($item instanceof \stdClass); + $this->assertObjectHasAttribute('content', $item); + $this->assertObjectHasAttribute('attributes', $item); + $this->assertEquals($string, $item->content); + } + + public function testCanBuildStyleTagsWithAttributes() + { + $this->helper->setStyle('a {}', array( + 'lang' => 'us_en', + 'title' => 'foo', + 'media' => 'projection', + 'dir' => 'rtol', + 'bogus' => 'unused' + )); + $value = $this->helper->getValue(); + + $this->assertObjectHasAttribute('attributes', $value); + $attributes = $value->attributes; + + $this->assertTrue(isset($attributes['lang'])); + $this->assertTrue(isset($attributes['title'])); + $this->assertTrue(isset($attributes['media'])); + $this->assertTrue(isset($attributes['dir'])); + $this->assertTrue(isset($attributes['bogus'])); + $this->assertEquals('us_en', $attributes['lang']); + $this->assertEquals('foo', $attributes['title']); + $this->assertEquals('projection', $attributes['media']); + $this->assertEquals('rtol', $attributes['dir']); + $this->assertEquals('unused', $attributes['bogus']); + } + + public function testRenderedStyleTagsContainHtmlEscaping() + { + $this->helper->setStyle('a {}', array( + 'lang' => 'us_en', + 'title' => 'foo', + 'media' => 'screen', + 'dir' => 'rtol', + 'bogus' => 'unused' + )); + $value = $this->helper->toString(); + $this->assertContains('', $value); + } + + public function testRenderedStyleTagsContainsDefaultMedia() + { + $this->helper->setStyle('a {}', array( + )); + $value = $this->helper->toString(); + $this->assertRegexp('#'); + $this->assertEquals(3, $styles); + $this->assertContains($style3, $html); + $this->assertContains($style2, $html); + $this->assertContains($style1, $html); + } + + public function testCapturingCapturesToObject() + { + $this->helper->captureStart(); + echo 'foobar'; + $this->helper->captureEnd(); + $values = $this->helper->getArrayCopy(); + $this->assertEquals(1, count($values)); + $item = array_shift($values); + $this->assertContains('foobar', $item->content); + } + + public function testOverloadingOffsetSetWritesToSpecifiedIndex() + { + $this->helper->offsetSetStyle(100, 'foobar'); + $values = $this->helper->getArrayCopy(); + $this->assertEquals(1, count($values)); + $this->assertTrue(isset($values[100])); + $item = $values[100]; + $this->assertContains('foobar', $item->content); + } + + public function testInvalidMethodRaisesException() + { + try { + $this->helper->bogusMethod(); + $this->fail('Invalid method should raise exception'); + } catch (View\Exception\ExceptionInterface $e) { } + } + + public function testTooFewArgumentsRaisesException() + { + try { + $this->helper->appendStyle(); + $this->fail('Too few arguments should raise exception'); + } catch (View\Exception\ExceptionInterface $e) { } + } + + public function testIndentationIsHonored() + { + $this->helper->setIndent(4); + $this->helper->appendStyle(' +a { + display: none; +}'); + $this->helper->appendStyle(' +h1 { + font-weight: bold +}'); + $string = $this->helper->toString(); + + $scripts = substr_count($string, ' assertEquals(2, $scripts); + $this->assertContains(' ' . PHP_EOL + . '' . PHP_EOL + . ''; + + $this->assertEquals($expected, $test); + } + + /** + * @group ZF-9532 + */ + public function testRenderConditionalCommentsShouldNotContainHtmlEscaping() + { + $style = 'a{display:none;}'; + $this->helper->appendStyle($style, array( + 'conditional' => 'IE 8' + )); + $value = $this->helper->toString(); + + $this->assertNotContains('', $value); + } +} diff --git a/test/Helper/HeadTitleTest.php b/test/Helper/HeadTitleTest.php new file mode 100644 index 00000000..e62dd9cf --- /dev/null +++ b/test/Helper/HeadTitleTest.php @@ -0,0 +1,231 @@ +basePath = __DIR__ . '/_files/modules'; + $this->helper = new Helper\HeadTitle(); + } + + /** + * Tears down the fixture, for example, close a network connection. + * This method is called after a test is executed. + * + * @return void + */ + public function tearDown() + { + unset($this->helper); + } + + public function testNamespaceRegisteredInPlaceholderRegistryAfterInstantiation() + { + $registry = Registry::getRegistry(); + if ($registry->containerExists('Zend_View_Helper_HeadTitle')) { + $registry->deleteContainer('Zend_View_Helper_HeadTitle'); + } + $this->assertFalse($registry->containerExists('Zend_View_Helper_HeadTitle')); + $helper = new Helper\HeadTitle(); + $this->assertTrue($registry->containerExists('Zend_View_Helper_HeadTitle')); + } + + public function testHeadTitleReturnsObjectInstance() + { + $placeholder = $this->helper->__invoke(); + $this->assertTrue($placeholder instanceof Helper\HeadTitle); + } + + public function testCanSetTitleViaHeadTitle() + { + $placeholder = $this->helper->__invoke('Foo Bar', 'SET'); + $this->assertContains('Foo Bar', $placeholder->toString()); + } + + public function testCanAppendTitleViaHeadTitle() + { + $placeholder = $this->helper->__invoke('Foo'); + $placeholder = $this->helper->__invoke('Bar'); + $this->assertContains('FooBar', $placeholder->toString()); + } + + public function testCanPrependTitleViaHeadTitle() + { + $placeholder = $this->helper->__invoke('Foo'); + $placeholder = $this->helper->__invoke('Bar', 'PREPEND'); + $this->assertContains('BarFoo', $placeholder->toString()); + } + + public function testReturnedPlaceholderToStringContainsFullTitleElement() + { + $placeholder = $this->helper->__invoke('Foo'); + $placeholder = $this->helper->__invoke('Bar', 'APPEND')->setSeparator(' :: '); + $this->assertEquals('Foo :: Bar', $placeholder->toString()); + } + + public function testToStringEscapesEntries() + { + $this->helper->__invoke(''); + $string = $this->helper->toString(); + $this->assertNotContains('assertNotContains('', $string); + } + + public function testToStringEscapesSeparator() + { + $this->helper->__invoke('Foo') + ->__invoke('Bar') + ->setSeparator('
    '); + $string = $this->helper->toString(); + $this->assertNotContains('
    ', $string); + $this->assertContains('Foo', $string); + $this->assertContains('Bar', $string); + $this->assertContains('br /', $string); + } + + public function testIndentationIsHonored() + { + $this->helper->setIndent(4); + $this->helper->__invoke('foo'); + $string = $this->helper->toString(); + + $this->assertContains(' ', $string); + } + + public function testAutoEscapeIsHonored() + { + $this->helper->__invoke('Some Title ©right;'); + $this->assertEquals('<title>Some Title &copyright;', $this->helper->toString()); + + $this->assertTrue($this->helper->__invoke()->getAutoEscape()); + $this->helper->__invoke()->setAutoEscape(false); + $this->assertFalse($this->helper->__invoke()->getAutoEscape()); + + + $this->assertEquals('Some Title ©right;', $this->helper->toString()); + } + + /** + * @issue ZF-2918 + * @link http://framework.zend.com/issues/browse/ZF-2918 + */ + public function testZF2918() + { + $this->helper->__invoke('Some Title'); + $this->helper->setPrefix('Prefix: '); + $this->helper->setPostfix(' :Postfix'); + + $this->assertEquals('Prefix: Some Title :Postfix', $this->helper->toString()); + } + + /** + * @issue ZF-3577 + * @link http://framework.zend.com/issues/browse/ZF-3577 + */ + public function testZF3577() + { + $this->helper->setAutoEscape(true); + $this->helper->__invoke('Some Title'); + $this->helper->setPrefix('Prefix & '); + $this->helper->setPostfix(' & Postfix'); + + $this->assertEquals('Prefix & Some Title & Postfix', $this->helper->toString()); + } + + public function testCanTranslateTitle() + { + $loader = new TestAsset\ArrayTranslator(); + $loader->translations = array( + 'Message_1' => 'Message 1 (en)', + ); + $translator = new Translator(); + $translator->getPluginManager()->setService('default', $loader); + $translator->addTranslationFile('default', null); + + $this->helper->setTranslatorEnabled(true); + $this->helper->setTranslator($translator); + $this->helper->__invoke('Message_1'); + $this->assertEquals('Message 1 (en)', $this->helper->toString()); + } + + public function testTranslatorMethods() + { + $translatorMock = $this->getMock('Zend\I18n\Translator\Translator'); + $this->helper->setTranslator($translatorMock, 'foo'); + + $this->assertEquals($translatorMock, $this->helper->getTranslator()); + $this->assertEquals('foo', $this->helper->getTranslatorTextDomain()); + $this->assertTrue($this->helper->hasTranslator()); + $this->assertTrue($this->helper->isTranslatorEnabled()); + + $this->helper->setTranslatorEnabled(false); + $this->assertFalse($this->helper->isTranslatorEnabled()); + } + + /** + * @group ZF-8036 + */ + public function testHeadTitleZero() + { + $this->helper->__invoke('0'); + $this->assertEquals('0', $this->helper->toString()); + } + + public function testCanPrependTitlesUsingDefaultAttachOrder() + { + $this->helper->setDefaultAttachOrder('PREPEND'); + $placeholder = $this->helper->__invoke('Foo'); + $placeholder = $this->helper->__invoke('Bar'); + $this->assertContains('BarFoo', $placeholder->toString()); + } + + + /** + * @group ZF-10284 + */ + public function testReturnTypeDefaultAttachOrder() + { + $this->assertTrue($this->helper->setDefaultAttachOrder('PREPEND') instanceof Helper\HeadTitle); + $this->assertEquals('PREPEND', $this->helper->getDefaultAttachOrder()); + } +} diff --git a/test/Helper/HtmlFlashTest.php b/test/Helper/HtmlFlashTest.php new file mode 100644 index 00000000..c673ac98 --- /dev/null +++ b/test/Helper/HtmlFlashTest.php @@ -0,0 +1,58 @@ +view = new View(); + $this->helper = new HtmlFlash(); + $this->helper->setView($this->view); + } + + public function tearDown() + { + unset($this->helper); + } + + public function testMakeHtmlFlash() + { + $htmlFlash = $this->helper->__invoke('/path/to/flash.swf'); + + $objectStartElement = ''; + + $this->assertContains($objectStartElement, $htmlFlash); + $this->assertContains('', $htmlFlash); + } +} + diff --git a/test/Helper/HtmlListTest.php b/test/Helper/HtmlListTest.php new file mode 100644 index 00000000..9917f423 --- /dev/null +++ b/test/Helper/HtmlListTest.php @@ -0,0 +1,225 @@ +view = new View(); + $this->helper = new Helper\HtmlList(); + $this->helper->setView($this->view); + } + + public function tearDown() + { + unset($this->helper); + } + + public function testMakeUnorderedList() + { + $items = array('one', 'two', 'three'); + + $list = $this->helper->__invoke($items); + + $this->assertContains('
      ', $list); + $this->assertContains('
    ', $list); + foreach ($items as $item) { + $this->assertContains('
  • ' . $item . '
  • ', $list); + } + } + + public function testMakeOrderedList() + { + $items = array('one', 'two', 'three'); + + $list = $this->helper->__invoke($items, true); + + $this->assertContains('
      ', $list); + $this->assertContains('
    ', $list); + foreach ($items as $item) { + $this->assertContains('
  • ' . $item . '
  • ', $list); + } + } + + public function testMakeUnorderedListWithAttribs() + { + $items = array('one', 'two', 'three'); + $attribs = array('class' => 'selected', 'name' => 'list'); + + $list = $this->helper->__invoke($items, false, $attribs); + + $this->assertContains('assertContains('class="selected"', $list); + $this->assertContains('name="list"', $list); + $this->assertContains('', $list); + foreach ($items as $item) { + $this->assertContains('
  • ' . $item . '
  • ', $list); + } + } + + public function testMakeOrderedListWithAttribs() + { + $items = array('one', 'two', 'three'); + $attribs = array('class' => 'selected', 'name' => 'list'); + + $list = $this->helper->__invoke($items, true, $attribs); + + $this->assertContains('assertContains('class="selected"', $list); + $this->assertContains('name="list"', $list); + $this->assertContains('', $list); + foreach ($items as $item) { + $this->assertContains('
  • ' . $item . '
  • ', $list); + } + } + + /* + * @group ZF-5018 + */ + public function testMakeNestedUnorderedList() + { + $items = array('one', array('four', 'five', 'six'), 'two', 'three'); + + $list = $this->helper->__invoke($items); + + $this->assertContains('
      ' . Helper\HtmlList::EOL, $list); + $this->assertContains('
    ' . Helper\HtmlList::EOL, $list); + $this->assertContains('one
      ' . Helper\HtmlList::EOL.'
    • four', $list); + $this->assertContains('
    • six
    • ' . Helper\HtmlList::EOL . '
    ' . + Helper\HtmlList::EOL . '' . Helper\HtmlList::EOL . '
  • two', $list); + } + + /* + * @group ZF-5018 + */ + public function testMakeNestedDeepUnorderedList() + { + $items = array('one', array('four', array('six', 'seven', 'eight'), 'five'), 'two', 'three'); + + $list = $this->helper->__invoke($items); + + $this->assertContains('
      ' . Helper\HtmlList::EOL, $list); + $this->assertContains('
    ' . Helper\HtmlList::EOL, $list); + $this->assertContains('one
      ' . Helper\HtmlList::EOL . '
    • four', $list); + $this->assertContains('
    • four
        ' . Helper\HtmlList::EOL . '
      • six', $list); + $this->assertContains('
      • five
      • ' . Helper\HtmlList::EOL . '
      ' . + Helper\HtmlList::EOL . '
    • ' . Helper\HtmlList::EOL . '
    • two', $list); + } + + public function testListWithValuesToEscapeForZF2283() + { + $items = array('one test', 'second & third', 'And \'some\' "final" test'); + + $list = $this->helper->__invoke($items); + + $this->assertContains('
        ', $list); + $this->assertContains('
      ', $list); + + $this->assertContains('
    • one <small> test
    • ', $list); + $this->assertContains('
    • second & third
    • ', $list); + $this->assertContains('
    • And 'some' "final" test
    • ', $list); + } + + public function testListEscapeSwitchedOffForZF2283() + { + $items = array('one small test'); + + $list = $this->helper->__invoke($items, false, false, false); + + $this->assertContains('
        ', $list); + $this->assertContains('
      ', $list); + + $this->assertContains('
    • one small test
    • ', $list); + } + + /** + * @group ZF-2527 + */ + public function testEscapeFlagHonoredForMultidimensionalLists() + { + $items = array('one', array('four', 'five', 'six'), 'two', 'three'); + + $list = $this->helper->__invoke($items, false, false, false); + + foreach ($items[1] as $item) { + $this->assertContains($item, $list); + } + } + + /** + * @group ZF-2527 + * Added the s modifier to match newlines after ZF-5018 + */ + public function testAttribsPassedIntoMultidimensionalLists() + { + $items = array('one', array('four', 'five', 'six'), 'two', 'three'); + + $list = $this->helper->__invoke($items, false, array('class' => 'foo')); + + foreach ($items[1] as $item) { + $this->assertRegexp('#]*?class="foo"[^>]*>.*?(
    • ' . $item . ')#s', $list); + } + + } + + /** + * @group ZF-2870 + */ + public function testEscapeFlagShouldBePassedRecursively() + { + $items = array( + 'one', + array( + 'four', + 'five', + 'six', + array( + 'two', + 'three', + ), + ), + ); + + $list = $this->helper->__invoke($items, false, false, false); + + $this->assertContains('
        ', $list); + $this->assertContains('
      ', $list); + + array_walk_recursive($items, array($this, 'validateItems'), $list); + } + + public function validateItems($value, $key, $userdata) + { + $this->assertContains('
    • ' . $value, $userdata); + } +} diff --git a/test/Helper/HtmlObjectTest.php b/test/Helper/HtmlObjectTest.php new file mode 100644 index 00000000..fdf431a8 --- /dev/null +++ b/test/Helper/HtmlObjectTest.php @@ -0,0 +1,119 @@ +view = new View(); + $this->helper = new HtmlObject(); + $this->helper->setView($this->view); + } + + public function tearDown() + { + unset($this->helper); + } + + public function testViewObjectIsSet() + { + $this->assertInstanceof('Zend\View\Renderer\RendererInterface', $this->helper->getView()); + } + + public function testMakeHtmlObjectWithoutAttribsWithoutParams() + { + $htmlObject = $this->helper->__invoke('datastring', 'typestring'); + + $this->assertContains('', $htmlObject); + $this->assertContains('', $htmlObject); + } + + public function testMakeHtmlObjectWithAttribsWithoutParams() + { + $attribs = array('attribkey1' => 'attribvalue1', + 'attribkey2' => 'attribvalue2'); + + $htmlObject = $this->helper->__invoke('datastring', 'typestring', $attribs); + + $this->assertContains('', $htmlObject); + $this->assertContains('', $htmlObject); + } + + public function testMakeHtmlObjectWithoutAttribsWithParamsHtml() + { + $this->view->plugin('doctype')->__invoke(Doctype::HTML4_STRICT); + + $params = array('paramname1' => 'paramvalue1', + 'paramname2' => 'paramvalue2'); + + $htmlObject = $this->helper->__invoke('datastring', 'typestring', array(), $params); + + $this->assertContains('', $htmlObject); + $this->assertContains('', $htmlObject); + + foreach ($params as $key => $value) { + $param = ''; + + $this->assertContains($param, $htmlObject); + } + } + + public function testMakeHtmlObjectWithoutAttribsWithParamsXhtml() + { + $this->view->plugin('doctype')->__invoke(Doctype::XHTML1_STRICT); + + $params = array('paramname1' => 'paramvalue1', + 'paramname2' => 'paramvalue2'); + + $htmlObject = $this->helper->__invoke('datastring', 'typestring', array(), $params); + + $this->assertContains('', $htmlObject); + $this->assertContains('', $htmlObject); + + foreach ($params as $key => $value) { + $param = ''; + + $this->assertContains($param, $htmlObject); + } + } + + public function testMakeHtmlObjectWithContent() + { + $htmlObject = $this->helper->__invoke('datastring', 'typestring', array(), array(), 'testcontent'); + + $this->assertContains('', $htmlObject); + $this->assertContains('testcontent', $htmlObject); + $this->assertContains('', $htmlObject); + } +} diff --git a/test/Helper/HtmlPageTest.php b/test/Helper/HtmlPageTest.php new file mode 100644 index 00000000..d5bdd8fe --- /dev/null +++ b/test/Helper/HtmlPageTest.php @@ -0,0 +1,59 @@ +view = new View(); + $this->helper = new HtmlPage(); + $this->helper->setView($this->view); + } + + public function tearDown() + { + unset($this->helper); + } + + public function testMakeHtmlPage() + { + $htmlPage = $this->helper->__invoke('/path/to/page.html'); + + $objectStartElement = ''; + + $this->assertContains($objectStartElement, $htmlPage); + $this->assertContains('', $htmlPage); + } +} diff --git a/test/Helper/HtmlQuicktimeTest.php b/test/Helper/HtmlQuicktimeTest.php new file mode 100644 index 00000000..25bad233 --- /dev/null +++ b/test/Helper/HtmlQuicktimeTest.php @@ -0,0 +1,60 @@ +view = new View(); + $this->helper = new HtmlQuicktime(); + $this->helper->setView($this->view); + } + + public function tearDown() + { + unset($this->helper); + } + + public function testMakeHtmlQuicktime() + { + $htmlQuicktime = $this->helper->__invoke('/path/to/quicktime.mov'); + + $objectStartElement = ''; + + $this->assertContains($objectStartElement, $htmlQuicktime); + $this->assertContains('', $htmlQuicktime); + } +} diff --git a/test/Helper/InlineScriptTest.php b/test/Helper/InlineScriptTest.php new file mode 100644 index 00000000..18c2e08d --- /dev/null +++ b/test/Helper/InlineScriptTest.php @@ -0,0 +1,78 @@ +basePath = __DIR__ . '/_files/modules'; + $this->helper = new Helper\InlineScript(); + } + + /** + * Tears down the fixture, for example, close a network connection. + * This method is called after a test is executed. + * + * @return void + */ + public function tearDown() + { + unset($this->helper); + } + + public function testNamespaceRegisteredInPlaceholderRegistryAfterInstantiation() + { + $registry = Registry::getRegistry(); + if ($registry->containerExists('Zend_View_Helper_InlineScript')) { + $registry->deleteContainer('Zend_View_Helper_InlineScript'); + } + $this->assertFalse($registry->containerExists('Zend_View_Helper_InlineScript')); + $helper = new Helper\InlineScript(); + $this->assertTrue($registry->containerExists('Zend_View_Helper_InlineScript')); + } + + public function testInlineScriptReturnsObjectInstance() + { + $placeholder = $this->helper->__invoke(); + $this->assertTrue($placeholder instanceof Helper\InlineScript); + } +} diff --git a/test/Helper/JsonTest.php b/test/Helper/JsonTest.php new file mode 100644 index 00000000..a9780e56 --- /dev/null +++ b/test/Helper/JsonTest.php @@ -0,0 +1,63 @@ +response = new Response(); + $this->helper = new JsonHelper(); + $this->helper->setResponse($this->response); + } + + public function verifyJsonHeader() + { + $headers = $this->response->getHeaders(); + $this->assertTrue($headers->has('Content-Type')); + $header = $headers->get('Content-Type'); + $this->assertEquals('application/json', $header->getFieldValue()); + } + + public function testJsonHelperSetsResponseHeader() + { + $json = $this->helper->__invoke('foobar'); + $this->verifyJsonHeader(); + } + + public function testJsonHelperReturnsJsonEncodedString() + { + $data = $this->helper->__invoke('foobar'); + $this->assertTrue(is_string($data)); + $this->assertEquals('foobar', JsonFormatter::decode($data)); + } +} diff --git a/test/Helper/LayoutTest.php b/test/Helper/LayoutTest.php new file mode 100644 index 00000000..91ab2895 --- /dev/null +++ b/test/Helper/LayoutTest.php @@ -0,0 +1,80 @@ +renderer = $renderer = new PhpRenderer(); + $this->viewModelHelper = $renderer->plugin('view_model'); + $this->helper = $renderer->plugin('layout'); + + $this->parent = new ViewModel(); + $this->parent->setTemplate('layout'); + $this->viewModelHelper->setRoot($this->parent); + } + + public function testCallingSetTemplateAltersRootModelTemplate() + { + $this->helper->setTemplate('alternate/layout'); + $this->assertEquals('alternate/layout', $this->parent->getTemplate()); + } + + public function testCallingGetLayoutReturnsRootModelTemplate() + { + $this->assertEquals('layout', $this->helper->getLayout()); + } + + public function testCallingInvokeProxiesToSetTemplate() + { + $helper = $this->helper; + $helper('alternate/layout'); + $this->assertEquals('alternate/layout', $this->parent->getTemplate()); + } + + public function testCallingInvokeWithNoArgumentReturnsViewModel() + { + $helper = $this->helper; + $result = $helper(); + $this->assertSame($this->parent, $result); + } + + public function testRaisesExceptionIfViewModelHelperHasNoRoot() + { + $renderer = new PhpRenderer(); + $viewModelHelper = $renderer->plugin('view_model'); + $helper = $renderer->plugin('layout'); + + $this->setExpectedException('Zend\View\Exception\RuntimeException', 'view model'); + $helper->setTemplate('foo/bar'); + } +} diff --git a/test/Helper/Navigation/AbstractTest.php b/test/Helper/Navigation/AbstractTest.php new file mode 100644 index 00000000..08d80835 --- /dev/null +++ b/test/Helper/Navigation/AbstractTest.php @@ -0,0 +1,207 @@ +_files = $cwd . '/_files'; + $config = ConfigFactory::fromFile($this->_files . '/navigation.xml', true); + + // setup containers from config + $this->_nav1 = new Navigation($config->get('nav_test1')); + $this->_nav2 = new Navigation($config->get('nav_test2')); + + // setup view + $view = new PhpRenderer(); + $view->resolver()->addPath($cwd . '/_files/mvc/views'); + + // create helper + $this->_helper = new $this->_helperName; + $this->_helper->setView($view); + + // set nav1 in helper as default + $this->_helper->setContainer($this->_nav1); + + // setup service manager + $smConfig = array( + 'modules' => array(), + 'module_listener_options' => array( + 'config_cache_enabled' => false, + 'cache_dir' => 'data/cache', + 'module_paths' => array(), + 'extra_config' => array( + 'service_manager' => array( + 'factories' => array( + 'Config' => function() use ($config) { + return array( + 'navigation' => array( + 'default' => $config->get('nav_test1'), + ), + ); + } + ), + ), + ), + ), + ); + + $sm = $this->serviceManager = new ServiceManager(new ServiceManagerConfig); + $sm->setService('ApplicationConfig', $smConfig); + $sm->get('ModuleManager')->loadModules(); + $sm->get('Application')->bootstrap(); + $sm->setFactory('Navigation', 'Zend\Navigation\Service\DefaultNavigationFactory'); + + $sm->setService('nav1', $this->_nav1); + $sm->setService('nav2', $this->_nav2); + + $app = $this->serviceManager->get('Application'); + $app->getMvcEvent()->setRouteMatch(new RouteMatch(array( + 'controller' => 'post', + 'action' => 'view', + 'id' => '1337', + ))); + } + + /** + * Returns the contens of the expected $file + * @param string $file + * @return string + */ + protected function _getExpected($file) + { + return file_get_contents($this->_files . '/expected/' . $file); + } + + /** + * Sets up ACL + * + * @return Acl + */ + protected function _getAcl() + { + $acl = new Acl(); + + $acl->addRole(new GenericRole('guest')); + $acl->addRole(new GenericRole('member'), 'guest'); + $acl->addRole(new GenericRole('admin'), 'member'); + $acl->addRole(new GenericRole('special'), 'member'); + + $acl->addResource(new GenericResource('guest_foo')); + $acl->addResource(new GenericResource('member_foo'), 'guest_foo'); + $acl->addResource(new GenericResource('admin_foo', 'member_foo')); + $acl->addResource(new GenericResource('special_foo'), 'member_foo'); + + $acl->allow('guest', 'guest_foo'); + $acl->allow('member', 'member_foo'); + $acl->allow('admin', 'admin_foo'); + $acl->allow('special', 'special_foo'); + $acl->allow('special', 'admin_foo', 'read'); + + return array('acl' => $acl, 'role' => 'special'); + } + + /** + * Returns translator + * + * @return Translator + */ + protected function _getTranslator() + { + $loader = new TestAsset\ArrayTranslator(); + $loader->translations = array( + 'Page 1' => 'Side 1', + 'Page 1.1' => 'Side 1.1', + 'Page 2' => 'Side 2', + 'Page 2.3' => 'Side 2.3', + 'Page 2.3.3.1' => 'Side 2.3.3.1', + 'Home' => 'Hjem', + 'Go home' => 'Gå hjem' + ); + $translator = new Translator(); + $translator->getPluginManager()->setService('default', $loader); + $translator->addTranslationFile('default', null); + return $translator; + } +} diff --git a/test/Helper/Navigation/BreadcrumbsTest.php b/test/Helper/Navigation/BreadcrumbsTest.php new file mode 100644 index 00000000..98826934 --- /dev/null +++ b/test/Helper/Navigation/BreadcrumbsTest.php @@ -0,0 +1,236 @@ +_helper->setServiceLocator($this->serviceManager); + + $returned = $this->_helper->renderStraight('Navigation'); + $this->assertEquals($returned, $this->_getExpected('bc/default.html')); + } + + public function testCanRenderPartialFromServiceAlias() + { + $this->_helper->setPartial('bc.phtml'); + $this->_helper->setServiceLocator($this->serviceManager); + + $returned = $this->_helper->renderPartial('Navigation'); + $this->assertEquals($returned, $this->_getExpected('bc/partial.html')); + } + + public function testHelperEntryPointWithoutAnyParams() + { + $returned = $this->_helper->__invoke(); + $this->assertEquals($this->_helper, $returned); + $this->assertEquals($this->_nav1, $returned->getContainer()); + } + + public function testHelperEntryPointWithContainerParam() + { + $returned = $this->_helper->__invoke($this->_nav2); + $this->assertEquals($this->_helper, $returned); + $this->assertEquals($this->_nav2, $returned->getContainer()); + } + + public function testHelperEntryPointWithContainerStringParam() + { + $pm = new \Zend\View\HelperPluginManager; + $pm->setServiceLocator($this->serviceManager); + $this->_helper->setServiceLocator($pm); + + $returned = $this->_helper->__invoke('nav1'); + $this->assertEquals($this->_helper, $returned); + $this->assertEquals($this->_nav1, $returned->getContainer()); + } + + public function testNullOutContainer() + { + $old = $this->_helper->getContainer(); + $this->_helper->setContainer(); + $new = $this->_helper->getContainer(); + + $this->assertNotEquals($old, $new); + } + + public function testSetSeparator() + { + $this->_helper->setSeparator('foo'); + + $expected = $this->_getExpected('bc/separator.html'); + $this->assertEquals($expected, $this->_helper->render()); + } + + public function testSetMaxDepth() + { + $this->_helper->setMaxDepth(1); + + $expected = $this->_getExpected('bc/maxdepth.html'); + $this->assertEquals($expected, $this->_helper->render()); + } + + public function testSetMinDepth() + { + $this->_helper->setMinDepth(1); + + $expected = ''; + $this->assertEquals($expected, $this->_helper->render($this->_nav2)); + } + + public function testLinkLastElement() + { + $this->_helper->setLinkLast(true); + + $expected = $this->_getExpected('bc/linklast.html'); + $this->assertEquals($expected, $this->_helper->render()); + } + + public function testSetIndent() + { + $this->_helper->setIndent(8); + + $expected = ' _helper->render(), 0, strlen($expected)); + + $this->assertEquals($expected, $actual); + } + + public function testRenderSuppliedContainerWithoutInterfering() + { + $this->_helper->setMinDepth(0); + + $rendered1 = $this->_getExpected('bc/default.html'); + $rendered2 = 'Site 2'; + + $expected = array( + 'registered' => $rendered1, + 'supplied' => $rendered2, + 'registered_again' => $rendered1 + ); + + $actual = array( + 'registered' => $this->_helper->render(), + 'supplied' => $this->_helper->render($this->_nav2), + 'registered_again' => $this->_helper->render() + ); + + $this->assertEquals($expected, $actual); + } + + public function testUseAclResourceFromPages() + { + $acl = $this->_getAcl(); + $this->_helper->setAcl($acl['acl']); + $this->_helper->setRole($acl['role']); + + $expected = $this->_getExpected('bc/acl.html'); + $this->assertEquals($expected, $this->_helper->render()); + } + + public function testTranslationUsingZendTranslate() + { + $this->_helper->setTranslator($this->_getTranslator()); + + $expected = $this->_getExpected('bc/translated.html'); + $this->assertEquals($expected, $this->_helper->render()); + } + + public function testTranslationUsingZendTranslateAdapter() + { + $translator = $this->_getTranslator(); + $this->_helper->setTranslator($translator); + + $expected = $this->_getExpected('bc/translated.html'); + $this->assertEquals($expected, $this->_helper->render()); + } + + public function testDisablingTranslation() + { + $translator = $this->_getTranslator(); + $this->_helper->setTranslator($translator); + $this->_helper->setTranslatorEnabled(false); + + $expected = $this->_getExpected('bc/default.html'); + $this->assertEquals($expected, $this->_helper->render()); + } + + public function testRenderingPartial() + { + $this->_helper->setPartial('bc.phtml'); + + $expected = $this->_getExpected('bc/partial.html'); + $this->assertEquals($expected, $this->_helper->render()); + } + + public function testRenderingPartialBySpecifyingAnArrayAsPartial() + { + $this->_helper->setPartial(array('bc.phtml', 'application')); + + $expected = $this->_getExpected('bc/partial.html'); + $this->assertEquals($expected, $this->_helper->render()); + } + + public function testRenderingPartialShouldFailOnInvalidPartialArray() + { + $this->_helper->setPartial(array('bc.phtml')); + + try { + $this->_helper->render(); + $this->fail( + '$partial was invalid, but no Zend\View\Exception\ExceptionInterface was thrown'); + } catch (ExceptionInterface $e) { + } + } + + public function testLastBreadcrumbShouldBeEscaped() + { + $container = new Navigation(array( + array( + 'label' => 'Live & Learn', + 'uri' => '#', + 'active' => true + ) + )); + + $expected = 'Live & Learn'; + $actual = $this->_helper->setMinDepth(0)->render($container); + + $this->assertEquals($expected, $actual); + } +} diff --git a/test/Helper/Navigation/LinksTest.php b/test/Helper/Navigation/LinksTest.php new file mode 100644 index 00000000..725f6348 --- /dev/null +++ b/test/Helper/Navigation/LinksTest.php @@ -0,0 +1,729 @@ +_doctypeHelper = $this->_helper->getView()->plugin('doctype'); + $this->_oldDoctype = $this->_doctypeHelper->getDoctype(); + $this->_doctypeHelper->setDoctype( + \Zend\View\Helper\Doctype::HTML4_LOOSE); + + // disable all active pages + foreach ($this->_helper->findAllByActive(true) as $page) { + $page->active = false; + } + } + + public function tearDown() + { + return; + $this->_doctypeHelper->setDoctype($this->_oldDoctype); + } + + public function testCanRenderFromServiceAlias() + { + $sm = $this->serviceManager; + $this->_helper->setServiceLocator($sm); + + $returned = $this->_helper->render('Navigation'); + $this->assertEquals($returned, $this->_getExpected('links/default.html')); + } + + public function testHelperEntryPointWithoutAnyParams() + { + $returned = $this->_helper->__invoke(); + $this->assertEquals($this->_helper, $returned); + $this->assertEquals($this->_nav1, $returned->getContainer()); + } + + public function testHelperEntryPointWithContainerParam() + { + $returned = $this->_helper->__invoke($this->_nav2); + $this->assertEquals($this->_helper, $returned); + $this->assertEquals($this->_nav2, $returned->getContainer()); + } + + public function testDoNotRenderIfNoPageIsActive() + { + $this->assertEquals('', $this->_helper->render()); + } + + public function testDetectRelationFromStringPropertyOfActivePage() + { + $active = $this->_helper->findOneByLabel('Page 2'); + $active->addRel('example', 'http://www.example.com/'); + $found = $this->_helper->findRelation($active, 'rel', 'example'); + + $expected = array( + 'type' => 'Zend\Navigation\Page\Uri', + 'href' => 'http://www.example.com/', + 'label' => null + ); + + $actual = array( + 'type' => get_class($found), + 'href' => $found->getHref(), + 'label' => $found->getLabel() + ); + + $this->assertEquals($expected, $actual); + } + + public function testDetectRelationFromPageInstancePropertyOfActivePage() + { + $active = $this->_helper->findOneByLabel('Page 2'); + $active->addRel('example', AbstractPage::factory(array( + 'uri' => 'http://www.example.com/', + 'label' => 'An example page' + ))); + $found = $this->_helper->findRelExample($active); + + $expected = array( + 'type' => 'Zend\Navigation\Page\Uri', + 'href' => 'http://www.example.com/', + 'label' => 'An example page' + ); + + $actual = array( + 'type' => get_class($found), + 'href' => $found->getHref(), + 'label' => $found->getLabel() + ); + + $this->assertEquals($expected, $actual); + } + + public function testDetectRelationFromArrayPropertyOfActivePage() + { + $active = $this->_helper->findOneByLabel('Page 2'); + $active->addRel('example', array( + 'uri' => 'http://www.example.com/', + 'label' => 'An example page' + )); + $found = $this->_helper->findRelExample($active); + + $expected = array( + 'type' => 'Zend\Navigation\Page\Uri', + 'href' => 'http://www.example.com/', + 'label' => 'An example page' + ); + + $actual = array( + 'type' => get_class($found), + 'href' => $found->getHref(), + 'label' => $found->getLabel() + ); + + $this->assertEquals($expected, $actual); + } + + public function testDetectRelationFromConfigInstancePropertyOfActivePage() + { + $active = $this->_helper->findOneByLabel('Page 2'); + $active->addRel('example', new Config\Config(array( + 'uri' => 'http://www.example.com/', + 'label' => 'An example page' + ))); + $found = $this->_helper->findRelExample($active); + + $expected = array( + 'type' => 'Zend\Navigation\Page\Uri', + 'href' => 'http://www.example.com/', + 'label' => 'An example page' + ); + + $actual = array( + 'type' => get_class($found), + 'href' => $found->getHref(), + 'label' => $found->getLabel() + ); + + $this->assertEquals($expected, $actual); + } + + public function testDetectMultipleRelationsFromArrayPropertyOfActivePage() + { + $active = $this->_helper->findOneByLabel('Page 2'); + + $active->addRel('alternate', array( + array( + 'label' => 'foo', + 'uri' => 'bar' + ), + array( + 'label' => 'baz', + 'uri' => 'bat' + ) + )); + + $found = $this->_helper->findRelAlternate($active); + + $expected = array('type' => 'array', 'count' => 2); + $actual = array('type' => gettype($found), 'count' => count($found)); + $this->assertEquals($expected, $actual); + } + + public function testDetectMultipleRelationsFromConfigPropertyOfActivePage() + { + $active = $this->_helper->findOneByLabel('Page 2'); + + $active->addRel('alternate', new Config\Config(array( + array( + 'label' => 'foo', + 'uri' => 'bar' + ), + array( + 'label' => 'baz', + 'uri' => 'bat' + ) + ))); + + $found = $this->_helper->findRelAlternate($active); + + $expected = array('type' => 'array', 'count' => 2); + $actual = array('type' => gettype($found), 'count' => count($found)); + $this->assertEquals($expected, $actual); + } + + public function testExtractingRelationsFromPageProperties() + { + $types = array( + 'alternate', 'stylesheet', 'start', 'next', 'prev', 'contents', + 'index', 'glossary', 'copyright', 'chapter', 'section', 'subsection', + 'appendix', 'help', 'bookmark' + ); + + $samplePage = AbstractPage::factory(array( + 'label' => 'An example page', + 'uri' => 'http://www.example.com/' + )); + + $active = $this->_helper->findOneByLabel('Page 2'); + $expected = array(); + $actual = array(); + + foreach ($types as $type) { + $active->addRel($type, $samplePage); + $found = $this->_helper->findRelation($active, 'rel', $type); + + $expected[$type] = $samplePage->getLabel(); + $actual[$type] = $found->getLabel(); + + $active->removeRel($type); + } + + $this->assertEquals($expected, $actual); + } + + public function testFindStartPageByTraversal() + { + $active = $this->_helper->findOneByLabel('Page 2.1'); + $expected = 'Home'; + $actual = $this->_helper->findRelStart($active)->getLabel(); + $this->assertEquals($expected, $actual); + } + + public function testDoNotFindStartWhenGivenPageIsTheFirstPage() + { + $active = $this->_helper->findOneByLabel('Home'); + $actual = $this->_helper->findRelStart($active); + $this->assertNull($actual, 'Should not find any start page'); + } + + public function testFindNextPageByTraversalShouldFindChildPage() + { + $active = $this->_helper->findOneByLabel('Page 2'); + $expected = 'Page 2.1'; + $actual = $this->_helper->findRelNext($active)->getLabel(); + $this->assertEquals($expected, $actual); + } + + public function testFindNextPageByTraversalShouldFindSiblingPage() + { + $active = $this->_helper->findOneByLabel('Page 2.1'); + $expected = 'Page 2.2'; + $actual = $this->_helper->findRelNext($active)->getLabel(); + $this->assertEquals($expected, $actual); + } + + public function testFindNextPageByTraversalShouldWrap() + { + $active = $this->_helper->findOneByLabel('Page 2.2.2'); + $expected = 'Page 2.3'; + $actual = $this->_helper->findRelNext($active)->getLabel(); + $this->assertEquals($expected, $actual); + } + + public function testFindPrevPageByTraversalShouldFindParentPage() + { + $active = $this->_helper->findOneByLabel('Page 2.1'); + $expected = 'Page 2'; + $actual = $this->_helper->findRelPrev($active)->getLabel(); + $this->assertEquals($expected, $actual); + } + + public function testFindPrevPageByTraversalShouldFindSiblingPage() + { + $active = $this->_helper->findOneByLabel('Page 2.2'); + $expected = 'Page 2.1'; + $actual = $this->_helper->findRelPrev($active)->getLabel(); + $this->assertEquals($expected, $actual); + } + + public function testFindPrevPageByTraversalShouldWrap() + { + $active = $this->_helper->findOneByLabel('Page 2.3'); + $expected = 'Page 2.2.2'; + $actual = $this->_helper->findRelPrev($active)->getLabel(); + $this->assertEquals($expected, $actual); + } + + public function testShouldFindChaptersFromFirstLevelOfPagesInContainer() + { + $active = $this->_helper->findOneByLabel('Page 2.3'); + $found = $this->_helper->findRelChapter($active); + + $expected = array('Page 1', 'Page 2', 'Page 3', 'Zym'); + $actual = array(); + foreach ($found as $page) { + $actual[] = $page->getLabel(); + } + + $this->assertEquals($expected, $actual); + } + + public function testFindingChaptersShouldExcludeSelfIfChapter() + { + $active = $this->_helper->findOneByLabel('Page 2'); + $found = $this->_helper->findRelChapter($active); + + $expected = array('Page 1', 'Page 3', 'Zym'); + $actual = array(); + foreach ($found as $page) { + $actual[] = $page->getLabel(); + } + + $this->assertEquals($expected, $actual); + } + + public function testFindSectionsWhenActiveChapterPage() + { + $active = $this->_helper->findOneByLabel('Page 2'); + $found = $this->_helper->findRelSection($active); + $expected = array('Page 2.1', 'Page 2.2', 'Page 2.3'); + $actual = array(); + foreach ($found as $page) { + $actual[] = $page->getLabel(); + } + $this->assertEquals($expected, $actual); + } + + public function testDoNotFindSectionsWhenActivePageIsASection() + { + $active = $this->_helper->findOneByLabel('Page 2.2'); + $found = $this->_helper->findRelSection($active); + $this->assertNull($found); + } + + public function testDoNotFindSectionsWhenActivePageIsASubsection() + { + $active = $this->_helper->findOneByLabel('Page 2.2.1'); + $found = $this->_helper->findRelation($active, 'rel', 'section'); + $this->assertNull($found); + } + + public function testFindSubsectionWhenActivePageIsSection() + { + $active = $this->_helper->findOneByLabel('Page 2.2'); + $found = $this->_helper->findRelSubsection($active); + + $expected = array('Page 2.2.1', 'Page 2.2.2'); + $actual = array(); + foreach ($found as $page) { + $actual[] = $page->getLabel(); + } + $this->assertEquals($expected, $actual); + } + + public function testDoNotFindSubsectionsWhenActivePageIsASubSubsection() + { + $active = $this->_helper->findOneByLabel('Page 2.2.1'); + $found = $this->_helper->findRelSubsection($active); + $this->assertNull($found); + } + + public function testDoNotFindSubsectionsWhenActivePageIsAChapter() + { + $active = $this->_helper->findOneByLabel('Page 2'); + $found = $this->_helper->findRelSubsection($active); + $this->assertNull($found); + } + + public function testFindRevSectionWhenPageIsSection() + { + $active = $this->_helper->findOneByLabel('Page 2.2'); + $found = $this->_helper->findRevSection($active); + $this->assertEquals('Page 2', $found->getLabel()); + } + + public function testFindRevSubsectionWhenPageIsSubsection() + { + $active = $this->_helper->findOneByLabel('Page 2.2.1'); + $found = $this->_helper->findRevSubsection($active); + $this->assertEquals('Page 2.2', $found->getLabel()); + } + + public function testAclFiltersAwayPagesFromPageProperty() + { + $acl = new Acl\Acl(); + $acl->addRole(new Role\GenericRole('member')); + $acl->addRole(new Role\GenericRole('admin')); + $acl->addResource(new Resource\GenericResource('protected')); + $acl->allow('admin', 'protected'); + $this->_helper->setAcl($acl); + $this->_helper->setRole($acl->getRole('member')); + + $samplePage = AbstractPage::factory(array( + 'label' => 'An example page', + 'uri' => 'http://www.example.com/', + 'resource' => 'protected' + )); + + $active = $this->_helper->findOneByLabel('Home'); + $expected = array( + 'alternate' => false, + 'stylesheet' => false, + 'start' => false, + 'next' => 'Page 1', + 'prev' => false, + 'contents' => false, + 'index' => false, + 'glossary' => false, + 'copyright' => false, + 'chapter' => 'array(4)', + 'section' => false, + 'subsection' => false, + 'appendix' => false, + 'help' => false, + 'bookmark' => false + ); + $actual = array(); + + foreach ($expected as $type => $discard) { + $active->addRel($type, $samplePage); + + $found = $this->_helper->findRelation($active, 'rel', $type); + if (null === $found) { + $actual[$type] = false; + } elseif (is_array($found)) { + $actual[$type] = 'array(' . count($found) . ')'; + } else { + $actual[$type] = $found->getLabel(); + } + } + + $this->assertEquals($expected, $actual); + } + + public function testAclFiltersAwayPagesFromContainerSearch() + { + $acl = new Acl\Acl(); + $acl->addRole(new Role\GenericRole('member')); + $acl->addRole(new Role\GenericRole('admin')); + $acl->addResource(new Resource\GenericResource('protected')); + $acl->allow('admin', 'protected'); + $this->_helper->setAcl($acl); + $this->_helper->setRole($acl->getRole('member')); + + $oldContainer = $this->_helper->getContainer(); + $container = $this->_helper->getContainer(); + $iterator = new \RecursiveIteratorIterator( + $container, + \RecursiveIteratorIterator::SELF_FIRST); + foreach ($iterator as $page) { + $page->resource = 'protected'; + } + $this->_helper->setContainer($container); + + $active = $this->_helper->findOneByLabel('Home'); + $search = array( + 'start' => 'Page 1', + 'next' => 'Page 1', + 'prev' => 'Page 1.1', + 'chapter' => 'Home', + 'section' => 'Page 1', + 'subsection' => 'Page 2.2' + ); + + $expected = array(); + $actual = array(); + + foreach ($search as $type => $active) { + $expected[$type] = false; + + $active = $this->_helper->findOneByLabel($active); + $found = $this->_helper->findRelation($active, 'rel', $type); + + if (null === $found) { + $actual[$type] = false; + } elseif (is_array($found)) { + $actual[$type] = 'array(' . count($found) . ')'; + } else { + $actual[$type] = $found->getLabel(); + } + } + + $this->assertEquals($expected, $actual); + } + + public function testFindRelationMustSpecifyRelOrRev() + { + $active = $this->_helper->findOneByLabel('Home'); + try { + $this->_helper->findRelation($active, 'foo', 'bar'); + $this->fail('An invalid value was given, but a ' . + 'Zend_View_Exception was not thrown'); + } catch (View\Exception\ExceptionInterface $e) { + $this->assertContains('Invalid argument: $rel', $e->getMessage()); + } + } + + public function testRenderLinkMustSpecifyRelOrRev() + { + $active = $this->_helper->findOneByLabel('Home'); + try { + $this->_helper->renderLink($active, 'foo', 'bar'); + $this->fail('An invalid value was given, but a ' . + 'Zend_View_Exception was not thrown'); + } catch (View\Exception\ExceptionInterface $e) { + $this->assertContains('Invalid relation attribute', $e->getMessage()); + } + } + + public function testFindAllRelations() + { + $expectedRelations = array( + 'alternate' => array('Forced page'), + 'stylesheet' => array('Forced page'), + 'start' => array('Forced page'), + 'next' => array('Forced page'), + 'prev' => array('Forced page'), + 'contents' => array('Forced page'), + 'index' => array('Forced page'), + 'glossary' => array('Forced page'), + 'copyright' => array('Forced page'), + 'chapter' => array('Forced page'), + 'section' => array('Forced page'), + 'subsection' => array('Forced page'), + 'appendix' => array('Forced page'), + 'help' => array('Forced page'), + 'bookmark' => array('Forced page'), + 'canonical' => array('Forced page'), + 'home' => array('Forced page') + ); + + // build expected result + $expected = array( + 'rel' => $expectedRelations, + 'rev' => $expectedRelations + ); + + // find active page and create page to use for relations + $active = $this->_helper->findOneByLabel('Page 1'); + $forcedRelation = new UriPage(array( + 'label' => 'Forced page', + 'uri' => '#' + )); + + // add relations to active page + foreach ($expectedRelations as $type => $discard) { + $active->addRel($type, $forcedRelation); + $active->addRev($type, $forcedRelation); + } + + // build actual result + $actual = $this->_helper->findAllRelations($active); + foreach ($actual as $attrib => $relations) { + foreach ($relations as $type => $pages) { + foreach ($pages as $key => $page) { + $actual[$attrib][$type][$key] = $page->getLabel(); + } + } + } + + $this->assertEquals($expected, $actual); + } + + private function _getFlags() + { + return array( + Navigation\Links::RENDER_ALTERNATE => 'alternate', + Navigation\Links::RENDER_STYLESHEET => 'stylesheet', + Navigation\Links::RENDER_START => 'start', + Navigation\Links::RENDER_NEXT => 'next', + Navigation\Links::RENDER_PREV => 'prev', + Navigation\Links::RENDER_CONTENTS => 'contents', + Navigation\Links::RENDER_INDEX => 'index', + Navigation\Links::RENDER_GLOSSARY => 'glossary', + Navigation\Links::RENDER_CHAPTER => 'chapter', + Navigation\Links::RENDER_SECTION => 'section', + Navigation\Links::RENDER_SUBSECTION => 'subsection', + Navigation\Links::RENDER_APPENDIX => 'appendix', + Navigation\Links::RENDER_HELP => 'help', + Navigation\Links::RENDER_BOOKMARK => 'bookmark', + Navigation\Links::RENDER_CUSTOM => 'canonical' + ); + } + + public function testSingleRenderFlags() + { + $active = $this->_helper->findOneByLabel('Home'); + $active->active = true; + + $expected = array(); + $actual = array(); + + // build expected and actual result + foreach ($this->_getFlags() as $newFlag => $type) { + // add forced relation + $active->addRel($type, 'http://www.example.com/'); + $active->addRev($type, 'http://www.example.com/'); + + $this->_helper->setRenderFlag($newFlag); + $expectedOutput = '' . constant($this->_helperName.'::EOL') + . ''; + $actualOutput = $this->_helper->render(); + + $expected[$type] = $expectedOutput; + $actual[$type] = $actualOutput; + + // remove forced relation + $active->removeRel($type); + $active->removeRev($type); + } + + $this->assertEquals($expected, $actual); + } + + public function testRenderFlagBitwiseOr() + { + $newFlag = Navigation\Links::RENDER_NEXT | + Navigation\Links::RENDER_PREV; + $this->_helper->setRenderFlag($newFlag); + $active = $this->_helper->findOneByLabel('Page 1.1'); + $active->active = true; + + // test data + $expected = '' + . constant($this->_helperName.'::EOL') + . ''; + $actual = $this->_helper->render(); + + $this->assertEquals($expected, $actual); + } + + public function testIndenting() + { + $active = $this->_helper->findOneByLabel('Page 1.1'); + $newFlag = Navigation\Links::RENDER_NEXT | + Navigation\Links::RENDER_PREV; + $this->_helper->setRenderFlag($newFlag); + $this->_helper->setIndent(' '); + $active->active = true; + + // build expected and actual result + $expected = ' ' + . constant($this->_helperName.'::EOL') + . ' '; + $actual = $this->_helper->render(); + + $this->assertEquals($expected, $actual); + } + + public function testSetMaxDepth() + { + $this->_helper->setMaxDepth(1); + $this->_helper->findOneByLabel('Page 2.3.3')->setActive(); // level 2 + $flag = Navigation\Links::RENDER_NEXT; + + $expected = ''; + $actual = $this->_helper->setRenderFlag($flag)->render(); + + $this->assertEquals($expected, $actual); + } + + public function testSetMinDepth() + { + $this->_helper->setMinDepth(2); + $this->_helper->findOneByLabel('Page 2.3')->setActive(); // level 1 + $flag = Navigation\Links::RENDER_NEXT; + + $expected = ''; + $actual = $this->_helper->setRenderFlag($flag)->render(); + + $this->assertEquals($expected, $actual); + } + + /** + * Returns the contens of the expected $file, normalizes newlines + * @param string $file + * @return string + */ + protected function _getExpected($file) + { + return str_replace("\n", PHP_EOL, parent::_getExpected($file)); + } +} diff --git a/test/Helper/Navigation/MenuTest.php b/test/Helper/Navigation/MenuTest.php new file mode 100644 index 00000000..6a5991b2 --- /dev/null +++ b/test/Helper/Navigation/MenuTest.php @@ -0,0 +1,532 @@ +_helper->setServiceLocator($this->serviceManager); + + $returned = $this->_helper->renderMenu('Navigation'); + $this->assertEquals($returned, $this->_getExpected('menu/default1.html')); + } + + public function testCanRenderPartialFromServiceAlias() + { + $this->_helper->setPartial('menu.phtml'); + $this->_helper->setServiceLocator($this->serviceManager); + + $returned = $this->_helper->renderPartial('Navigation'); + $this->assertEquals($returned, $this->_getExpected('menu/partial.html')); + } + + public function testHelperEntryPointWithoutAnyParams() + { + $returned = $this->_helper->__invoke(); + $this->assertEquals($this->_helper, $returned); + $this->assertEquals($this->_nav1, $returned->getContainer()); + } + + public function testHelperEntryPointWithContainerParam() + { + $returned = $this->_helper->__invoke($this->_nav2); + $this->assertEquals($this->_helper, $returned); + $this->assertEquals($this->_nav2, $returned->getContainer()); + } + + public function testNullingOutContainerInHelper() + { + $this->_helper->setContainer(); + $this->assertEquals(0, count($this->_helper->getContainer())); + } + + public function testSetIndentAndOverrideInRenderMenu() + { + $this->_helper->setIndent(8); + + $expected = array( + 'indent4' => $this->_getExpected('menu/indent4.html'), + 'indent8' => $this->_getExpected('menu/indent8.html') + ); + + $renderOptions = array( + 'indent' => 4 + ); + + $actual = array( + 'indent4' => rtrim($this->_helper->renderMenu(null, $renderOptions), PHP_EOL), + 'indent8' => rtrim($this->_helper->renderMenu(), PHP_EOL) + ); + + $this->assertEquals($expected, $actual); + } + + public function testRenderSuppliedContainerWithoutInterfering() + { + $rendered1 = $this->_getExpected('menu/default1.html'); + $rendered2 = $this->_getExpected('menu/default2.html'); + $expected = array( + 'registered' => $rendered1, + 'supplied' => $rendered2, + 'registered_again' => $rendered1 + ); + + $actual = array( + 'registered' => $this->_helper->render(), + 'supplied' => $this->_helper->render($this->_nav2), + 'registered_again' => $this->_helper->render() + ); + + $this->assertEquals($expected, $actual); + } + + public function testUseAclRoleAsString() + { + $acl = $this->_getAcl(); + $this->_helper->setAcl($acl['acl']); + $this->_helper->setRole('member'); + + $expected = $this->_getExpected('menu/acl_string.html'); + $this->assertEquals($expected, $this->_helper->render()); + } + + public function testFilterOutPagesBasedOnAcl() + { + $acl = $this->_getAcl(); + $this->_helper->setAcl($acl['acl']); + $this->_helper->setRole($acl['role']); + + $expected = $this->_getExpected('menu/acl.html'); + $actual = $this->_helper->render(); + + $this->assertEquals($expected, $actual); + } + + public function testDisablingAcl() + { + $acl = $this->_getAcl(); + $this->_helper->setAcl($acl['acl']); + $this->_helper->setRole($acl['role']); + $this->_helper->setUseAcl(false); + + $expected = $this->_getExpected('menu/default1.html'); + $actual = $this->_helper->render(); + + $this->assertEquals($expected, $actual); + } + + public function testUseAnAclRoleInstanceFromAclObject() + { + $acl = $this->_getAcl(); + $this->_helper->setAcl($acl['acl']); + $this->_helper->setRole($acl['acl']->getRole('member')); + + $expected = $this->_getExpected('menu/acl_role_interface.html'); + $this->assertEquals($expected, $this->_helper->render()); + } + + public function testUseConstructedAclRolesNotFromAclObject() + { + $acl = $this->_getAcl(); + $this->_helper->setAcl($acl['acl']); + $this->_helper->setRole(new \Zend\Permissions\Acl\Role\GenericRole('member')); + + $expected = $this->_getExpected('menu/acl_role_interface.html'); + $this->assertEquals($expected, $this->_helper->render()); + } + + public function testSetUlCssClass() + { + $this->_helper->setUlClass('My_Nav'); + $expected = $this->_getExpected('menu/css.html'); + $this->assertEquals($expected, $this->_helper->render($this->_nav2)); + } + + public function testOptionEscapeLabelsAsTrue() + { + $options = array( + 'escapeLabels' => true + ); + + $container = new \Zend\Navigation\Navigation($this->_nav2->toArray()); + $container->addPage(array( + 'label' => 'Badges 1', + 'uri' => 'badges' + )); + + $expected = $this->_getExpected('menu/escapelabels_as_true.html'); + $actual = $this->_helper->renderMenu($container, $options); + + $this->assertEquals($expected, $actual); + } + + public function testOptionEscapeLabelsAsFalse() + { + $options = array( + 'escapeLabels' => false + ); + + $container = new \Zend\Navigation\Navigation($this->_nav2->toArray()); + $container->addPage(array( + 'label' => 'Badges 1', + 'uri' => 'badges' + )); + + $expected = $this->_getExpected('menu/escapelabels_as_false.html'); + $actual = $this->_helper->renderMenu($container, $options); + + $this->assertEquals($expected, $actual); + } + + public function testTranslationUsingZendTranslate() + { + $translator = $this->_getTranslator(); + $this->_helper->setTranslator($translator); + + $expected = $this->_getExpected('menu/translated.html'); + $this->assertEquals($expected, $this->_helper->render()); + } + + public function testTranslationUsingZendTranslateAdapter() + { + $translator = $this->_getTranslator(); + $this->_helper->setTranslator($translator); + + $expected = $this->_getExpected('menu/translated.html'); + $this->assertEquals($expected, $this->_helper->render()); + } + + public function testDisablingTranslation() + { + $translator = $this->_getTranslator(); + $this->_helper->setTranslator($translator); + $this->_helper->setTranslatorEnabled(false); + + $expected = $this->_getExpected('menu/default1.html'); + $this->assertEquals($expected, $this->_helper->render()); + } + + public function testRenderingPartial() + { + $this->_helper->setPartial('menu.phtml'); + + $expected = $this->_getExpected('menu/partial.html'); + $actual = $this->_helper->render(); + + $this->assertEquals($expected, $actual); + } + + public function testRenderingPartialBySpecifyingAnArrayAsPartial() + { + $this->_helper->setPartial(array('menu.phtml', 'application')); + + $expected = $this->_getExpected('menu/partial.html'); + $actual = $this->_helper->render(); + + $this->assertEquals($expected, $actual); + } + + public function testRenderingPartialShouldFailOnInvalidPartialArray() + { + $this->_helper->setPartial(array('menu.phtml')); + + try { + $this->_helper->render(); + $this->fail('invalid $partial should throw Zend_View_Exception'); + } catch (\Zend\View\Exception\ExceptionInterface $e) { + } + } + + public function testSetMaxDepth() + { + $this->_helper->setMaxDepth(1); + + $expected = $this->_getExpected('menu/maxdepth.html'); + $actual = $this->_helper->renderMenu(); + + $this->assertEquals($expected, $actual); + } + + public function testSetMinDepth() + { + $this->_helper->setMinDepth(1); + + $expected = $this->_getExpected('menu/mindepth.html'); + $actual = $this->_helper->renderMenu(); + + $this->assertEquals($expected, $actual); + } + + public function testSetBothDepts() + { + $this->_helper->setMinDepth(1)->setMaxDepth(2); + + $expected = $this->_getExpected('menu/bothdepts.html'); + $actual = $this->_helper->renderMenu(); + + $this->assertEquals($expected, $actual); + } + + public function testSetOnlyActiveBranch() + { + $this->_helper->setOnlyActiveBranch(true); + + $expected = $this->_getExpected('menu/onlyactivebranch.html'); + $actual = $this->_helper->renderMenu(); + + $this->assertEquals($expected, $actual); + } + + public function testSetRenderParents() + { + $this->_helper->setOnlyActiveBranch(true)->setRenderParents(false); + + $expected = $this->_getExpected('menu/onlyactivebranch_noparents.html'); + $actual = $this->_helper->renderMenu(); + + $this->assertEquals($expected, $actual); + } + + public function testSetOnlyActiveBranchAndMinDepth() + { + $this->_helper->setOnlyActiveBranch()->setMinDepth(1); + + $expected = $this->_getExpected('menu/onlyactivebranch_mindepth.html'); + $actual = $this->_helper->renderMenu(); + + $this->assertEquals($expected, $actual); + } + + public function testOnlyActiveBranchAndMaxDepth() + { + $this->_helper->setOnlyActiveBranch()->setMaxDepth(2); + + $expected = $this->_getExpected('menu/onlyactivebranch_maxdepth.html'); + $actual = $this->_helper->renderMenu(); + + $this->assertEquals($expected, $actual); + } + + public function testOnlyActiveBranchAndBothDepthsSpecified() + { + $this->_helper->setOnlyActiveBranch()->setMinDepth(1)->setMaxDepth(2); + + $expected = $this->_getExpected('menu/onlyactivebranch_bothdepts.html'); + $actual = $this->_helper->renderMenu(); + + $this->assertEquals($expected, $actual); + } + + public function testOnlyActiveBranchNoParentsAndBothDepthsSpecified() + { + $this->_helper->setOnlyActiveBranch() + ->setMinDepth(1) + ->setMaxDepth(2) + ->setRenderParents(false); + + $expected = $this->_getExpected('menu/onlyactivebranch_np_bd.html'); + $actual = $this->_helper->renderMenu(); + + $this->assertEquals($expected, $actual); + } + + private function _setActive($label) + { + $container = $this->_helper->getContainer(); + + foreach ($container->findAllByActive(true) as $page) { + $page->setActive(false); + } + + if ($p = $container->findOneByLabel($label)) { + $p->setActive(true); + } + } + + public function testOnlyActiveBranchNoParentsActiveOneBelowMinDepth() + { + $this->_setActive('Page 2'); + + $this->_helper->setOnlyActiveBranch() + ->setMinDepth(1) + ->setMaxDepth(1) + ->setRenderParents(false); + + $expected = $this->_getExpected('menu/onlyactivebranch_np_bd2.html'); + $actual = $this->_helper->renderMenu(); + + $this->assertEquals($expected, $actual); + } + + public function testRenderSubMenuShouldOverrideOptions() + { + $this->_helper->setOnlyActiveBranch(false) + ->setMinDepth(1) + ->setMaxDepth(2) + ->setRenderParents(true); + + $expected = $this->_getExpected('menu/onlyactivebranch_noparents.html'); + $actual = $this->_helper->renderSubMenu(); + + $this->assertEquals($expected, $actual); + } + + public function testOptionMaxDepth() + { + $options = array( + 'maxDepth' => 1 + ); + + $expected = $this->_getExpected('menu/maxdepth.html'); + $actual = $this->_helper->renderMenu(null, $options); + + $this->assertEquals($expected, $actual); + } + + public function testOptionMinDepth() + { + $options = array( + 'minDepth' => 1 + ); + + $expected = $this->_getExpected('menu/mindepth.html'); + $actual = $this->_helper->renderMenu(null, $options); + + $this->assertEquals($expected, $actual); + } + + public function testOptionBothDepts() + { + $options = array( + 'minDepth' => 1, + 'maxDepth' => 2 + ); + + $expected = $this->_getExpected('menu/bothdepts.html'); + $actual = $this->_helper->renderMenu(null, $options); + + $this->assertEquals($expected, $actual); + } + + public function testOptionOnlyActiveBranch() + { + $options = array( + 'onlyActiveBranch' => true + ); + + $expected = $this->_getExpected('menu/onlyactivebranch.html'); + $actual = $this->_helper->renderMenu(null, $options); + + $this->assertEquals($expected, $actual); + } + + public function testOptionOnlyActiveBranchNoParents() + { + $options = array( + 'onlyActiveBranch' => true, + 'renderParents' => false + ); + + $expected = $this->_getExpected('menu/onlyactivebranch_noparents.html'); + $actual = $this->_helper->renderMenu(null, $options); + + $this->assertEquals($expected, $actual); + } + + public function testOptionOnlyActiveBranchAndMinDepth() + { + $options = array( + 'minDepth' => 1, + 'onlyActiveBranch' => true + ); + + $expected = $this->_getExpected('menu/onlyactivebranch_mindepth.html'); + $actual = $this->_helper->renderMenu(null, $options); + + $this->assertEquals($expected, $actual); + } + + public function testOptionOnlyActiveBranchAndMaxDepth() + { + $options = array( + 'maxDepth' => 2, + 'onlyActiveBranch' => true + ); + + $expected = $this->_getExpected('menu/onlyactivebranch_maxdepth.html'); + $actual = $this->_helper->renderMenu(null, $options); + + $this->assertEquals($expected, $actual); + } + + public function testOptionOnlyActiveBranchAndBothDepthsSpecified() + { + $options = array( + 'minDepth' => 1, + 'maxDepth' => 2, + 'onlyActiveBranch' => true + ); + + $expected = $this->_getExpected('menu/onlyactivebranch_bothdepts.html'); + $actual = $this->_helper->renderMenu(null, $options); + + $this->assertEquals($expected, $actual); + } + + public function testOptionOnlyActiveBranchNoParentsAndBothDepthsSpecified() + { + $options = array( + 'minDepth' => 2, + 'maxDepth' => 2, + 'onlyActiveBranch' => true, + 'renderParents' => false + ); + + $expected = $this->_getExpected('menu/onlyactivebranch_np_bd.html'); + $actual = $this->_helper->renderMenu(null, $options); + + $this->assertEquals($expected, $actual); + } + + /** + * Returns the contens of the expected $file, normalizes newlines + * @param string $file + * @return string + */ + protected function _getExpected($file) + { + return str_replace("\n", PHP_EOL, parent::_getExpected($file)); + } +} diff --git a/test/Helper/Navigation/NavigationTest.php b/test/Helper/Navigation/NavigationTest.php new file mode 100644 index 00000000..53805c98 --- /dev/null +++ b/test/Helper/Navigation/NavigationTest.php @@ -0,0 +1,453 @@ +_helper->__invoke(); + $this->assertEquals($this->_helper, $returned); + $this->assertEquals($this->_nav1, $returned->getContainer()); + } + + public function testHelperEntryPointWithContainerParam() + { + $returned = $this->_helper->__invoke($this->_nav2); + $this->assertEquals($this->_helper, $returned); + $this->assertEquals($this->_nav2, $returned->getContainer()); + } + + public function testAcceptAclShouldReturnGracefullyWithUnknownResource() + { + // setup + $acl = $this->_getAcl(); + $this->_helper->setAcl($acl['acl']); + $this->_helper->setRole($acl['role']); + + $accepted = $this->_helper->accept( + new \Zend\Navigation\Page\Uri(array( + 'resource' => 'unknownresource', + 'privilege' => 'someprivilege' + ), + false) + ); + + $this->assertEquals($accepted, false); + } + + public function testShouldProxyToMenuHelperByDefault() + { + $this->_helper->setContainer($this->_nav1); + + // result + $expected = $this->_getExpected('menu/default1.html'); + $actual = $this->_helper->render(); + + $this->assertEquals($expected, $actual); + } + + public function testHasContainer() + { + $oldContainer = $this->_helper->getContainer(); + $this->_helper->setContainer(null); + $this->assertFalse($this->_helper->hasContainer()); + $this->_helper->setContainer($oldContainer); + } + + public function testInjectingContainer() + { + // setup + $this->_helper->setContainer($this->_nav2); + $expected = array( + 'menu' => $this->_getExpected('menu/default2.html'), + 'breadcrumbs' => $this->_getExpected('bc/default.html') + ); + $actual = array(); + + // result + $actual['menu'] = $this->_helper->render(); + $this->_helper->setContainer($this->_nav1); + $actual['breadcrumbs'] = $this->_helper->breadcrumbs()->render(); + + $this->assertEquals($expected, $actual); + } + + public function testDisablingContainerInjection() + { + // setup + $this->_helper->setInjectContainer(false); + $this->_helper->menu()->setContainer(null); + $this->_helper->breadcrumbs()->setContainer(null); + $this->_helper->setContainer($this->_nav2); + + // result + $expected = array( + 'menu' => '', + 'breadcrumbs' => '' + ); + $actual = array( + 'menu' => $this->_helper->render(), + 'breadcrumbs' => $this->_helper->breadcrumbs()->render() + ); + + $this->assertEquals($expected, $actual); + } + + public function testServiceManagerIsUsedToRetrieveContainer() + { + $container = new Container; + $serviceManager = new ServiceManager; + $serviceManager->setService('navigation', $container); + + $pluginManager = new View\HelperPluginManager; + $pluginManager->setServiceLocator($serviceManager); + + $this->_helper->setServiceLocator($pluginManager); + $this->_helper->setContainer('navigation'); + + $expected = $this->_helper->getContainer(); + $actual = $container; + $this->assertEquals($expected, $actual); + } + + public function testInjectingAcl() + { + // setup + $acl = $this->_getAcl(); + $this->_helper->setAcl($acl['acl']); + $this->_helper->setRole($acl['role']); + + $expected = $this->_getExpected('menu/acl.html'); + $actual = $this->_helper->render(); + + $this->assertEquals($expected, $actual); + } + + public function testDisablingAclInjection() + { + // setup + $acl = $this->_getAcl(); + $this->_helper->setAcl($acl['acl']); + $this->_helper->setRole($acl['role']); + $this->_helper->setInjectAcl(false); + + $expected = $this->_getExpected('menu/default1.html'); + $actual = $this->_helper->render(); + + $this->assertEquals($expected, $actual); + } + + public function testInjectingTranslator() + { + $this->_helper->setTranslator($this->_getTranslator()); + + $expected = $this->_getExpected('menu/translated.html'); + $actual = $this->_helper->render(); + + $this->assertEquals($expected, $actual); + } + + public function testDisablingTranslatorInjection() + { + $this->_helper->setTranslator($this->_getTranslator()); + $this->_helper->setInjectTranslator(false); + + $expected = $this->_getExpected('menu/default1.html'); + $actual = $this->_helper->render(); + + $this->assertEquals($expected, $actual); + } + + public function testTranslatorMethods() + { + $translatorMock = $this->getMock('Zend\I18n\Translator\Translator'); + $this->_helper->setTranslator($translatorMock, 'foo'); + + $this->assertEquals($translatorMock, $this->_helper->getTranslator()); + $this->assertEquals('foo', $this->_helper->getTranslatorTextDomain()); + $this->assertTrue($this->_helper->hasTranslator()); + $this->assertTrue($this->_helper->isTranslatorEnabled()); + + $this->_helper->setTranslatorEnabled(false); + $this->assertFalse($this->_helper->isTranslatorEnabled()); + } + + public function testSpecifyingDefaultProxy() + { + $expected = array( + 'breadcrumbs' => $this->_getExpected('bc/default.html'), + 'menu' => $this->_getExpected('menu/default1.html') + ); + $actual = array(); + + // result + $this->_helper->setDefaultProxy('breadcrumbs'); + $actual['breadcrumbs'] = $this->_helper->render($this->_nav1); + $this->_helper->setDefaultProxy('menu'); + $actual['menu'] = $this->_helper->render($this->_nav1); + + $this->assertEquals($expected, $actual); + } + + public function testGetAclReturnsNullIfNoAclInstance() + { + $this->assertNull($this->_helper->getAcl()); + } + + public function testGetAclReturnsAclInstanceSetWithSetAcl() + { + $acl = new Acl\Acl(); + $this->_helper->setAcl($acl); + $this->assertEquals($acl, $this->_helper->getAcl()); + } + + public function testGetAclReturnsAclInstanceSetWithSetDefaultAcl() + { + $acl = new Acl\Acl(); + Navigation\AbstractHelper::setDefaultAcl($acl); + $actual = $this->_helper->getAcl(); + Navigation\AbstractHelper::setDefaultAcl(null); + $this->assertEquals($acl, $actual); + } + + public function testSetDefaultAclAcceptsNull() + { + $acl = new Acl\Acl(); + Navigation\AbstractHelper::setDefaultAcl($acl); + Navigation\AbstractHelper::setDefaultAcl(null); + $this->assertNull($this->_helper->getAcl()); + } + + public function testSetDefaultAclAcceptsNoParam() + { + $acl = new Acl\Acl(); + Navigation\AbstractHelper::setDefaultAcl($acl); + Navigation\AbstractHelper::setDefaultAcl(); + $this->assertNull($this->_helper->getAcl()); + } + + public function testSetRoleAcceptsString() + { + $this->_helper->setRole('member'); + $this->assertEquals('member', $this->_helper->getRole()); + } + + public function testSetRoleAcceptsRoleInterface() + { + $role = new Role\GenericRole('member'); + $this->_helper->setRole($role); + $this->assertEquals($role, $this->_helper->getRole()); + } + + public function testSetRoleAcceptsNull() + { + $this->_helper->setRole('member')->setRole(null); + $this->assertNull($this->_helper->getRole()); + } + + public function testSetRoleAcceptsNoParam() + { + $this->_helper->setRole('member')->setRole(); + $this->assertNull($this->_helper->getRole()); + } + + public function testSetRoleThrowsExceptionWhenGivenAnInt() + { + try { + $this->_helper->setRole(1337); + $this->fail('An invalid argument was given, but a ' . + 'Zend_View_Exception was not thrown'); + } catch (View\Exception\ExceptionInterface $e) { + $this->assertContains('$role must be a string', $e->getMessage()); + } + } + + public function testSetRoleThrowsExceptionWhenGivenAnArbitraryObject() + { + try { + $this->_helper->setRole(new \stdClass()); + $this->fail('An invalid argument was given, but a ' . + 'Zend_View_Exception was not thrown'); + } catch (View\Exception\ExceptionInterface $e) { + $this->assertContains('$role must be a string', $e->getMessage()); + } + } + + public function testSetDefaultRoleAcceptsString() + { + $expected = 'member'; + Navigation\AbstractHelper::setDefaultRole($expected); + $actual = $this->_helper->getRole(); + Navigation\AbstractHelper::setDefaultRole(null); + $this->assertEquals($expected, $actual); + } + + public function testSetDefaultRoleAcceptsRoleInterface() + { + $expected = new Role\GenericRole('member'); + Navigation\AbstractHelper::setDefaultRole($expected); + $actual = $this->_helper->getRole(); + Navigation\AbstractHelper::setDefaultRole(null); + $this->assertEquals($expected, $actual); + } + + public function testSetDefaultRoleAcceptsNull() + { + Navigation\AbstractHelper::setDefaultRole(null); + $this->assertNull($this->_helper->getRole()); + } + + public function testSetDefaultRoleAcceptsNoParam() + { + Navigation\AbstractHelper::setDefaultRole(); + $this->assertNull($this->_helper->getRole()); + } + + public function testSetDefaultRoleThrowsExceptionWhenGivenAnInt() + { + try { + Navigation\AbstractHelper::setDefaultRole(1337); + $this->fail('An invalid argument was given, but a ' . + 'Zend_View_Exception was not thrown'); + } catch (View\Exception\ExceptionInterface $e) { + $this->assertContains('$role must be', $e->getMessage()); + } + } + + public function testSetDefaultRoleThrowsExceptionWhenGivenAnArbitraryObject() + { + try { + Navigation\AbstractHelper::setDefaultRole(new \stdClass()); + $this->fail('An invalid argument was given, but a ' . + 'Zend_View_Exception was not thrown'); + } catch (View\Exception\ExceptionInterface $e) { + $this->assertContains('$role must be', $e->getMessage()); + } + } + + private $_errorMessage; + public function toStringErrorHandler($code, $msg, $file, $line, array $c) + { + $this->_errorMessage = $msg; + } + + public function testMagicToStringShouldNotThrowException() + { + set_error_handler(array($this, 'toStringErrorHandler')); + $this->_helper->menu()->setPartial(array(1337)); + $this->_helper->__toString(); + restore_error_handler(); + + $this->assertContains('array must contain two values', $this->_errorMessage); + } + + public function testPageIdShouldBeNormalized() + { + $nl = PHP_EOL; + + $container = new \Zend\Navigation\Navigation(array( + array( + 'label' => 'Page 1', + 'id' => 'p1', + 'uri' => 'p1' + ), + array( + 'label' => 'Page 2', + 'id' => 'p2', + 'uri' => 'p2' + ) + )); + + $expected = ''; + + $actual = $this->_helper->render($container); + + $this->assertEquals($expected, $actual); + } + + /** + * @group ZF-6854 + */ + public function testRenderInvisibleItem() + { + $container = new \Zend\Navigation\Navigation(array( + array( + 'label' => 'Page 1', + 'id' => 'p1', + 'uri' => 'p1' + ), + array( + 'label' => 'Page 2', + 'id' => 'p2', + 'uri' => 'p2', + 'visible' => false + ) + )); + + $render = $this->_helper->menu()->render($container); + + $this->assertFalse(strpos($render, 'p2')); + + $this->_helper->menu()->setRenderInvisible(); + + $render = $this->_helper->menu()->render($container); + + $this->assertTrue(strpos($render, 'p2') !== false); + } + + /** + * Returns the contens of the expected $file, normalizes newlines + * @param string $file + * @return string + */ + protected function _getExpected($file) + { + return str_replace("\n", PHP_EOL, parent::_getExpected($file)); + } +} diff --git a/test/Helper/Navigation/SitemapTest.php b/test/Helper/Navigation/SitemapTest.php new file mode 100644 index 00000000..f85c5c6e --- /dev/null +++ b/test/Helper/Navigation/SitemapTest.php @@ -0,0 +1,262 @@ +_originaltimezone = date_default_timezone_get(); + date_default_timezone_set('Europe/Berlin'); + + if (isset($_SERVER['SERVER_NAME'])) { + $this->_oldServer['SERVER_NAME'] = $_SERVER['SERVER_NAME']; + } + + if (isset($_SERVER['SERVER_PORT'])) { + $this->_oldServer['SERVER_PORT'] = $_SERVER['SERVER_PORT']; + } + + if (isset($_SERVER['REQUEST_URI'])) { + $this->_oldServer['REQUEST_URI'] = $_SERVER['REQUEST_URI']; + } + + $_SERVER['SERVER_NAME'] = 'localhost'; + $_SERVER['SERVER_PORT'] = 80; + $_SERVER['REQUEST_URI'] = '/'; + + parent::setUp(); + + $this->_helper->setFormatOutput(true); + $this->_helper->getView()->plugin('basepath')->setBasePath(''); + } + + protected function tearDown() + { + foreach ($this->_oldServer as $key => $value) { + $_SERVER[$key] = $value; + } + date_default_timezone_set($this->_originaltimezone); + } + + public function testHelperEntryPointWithoutAnyParams() + { + $returned = $this->_helper->__invoke(); + $this->assertEquals($this->_helper, $returned); + $this->assertEquals($this->_nav1, $returned->getContainer()); + } + + public function testHelperEntryPointWithContainerParam() + { + $returned = $this->_helper->__invoke($this->_nav2); + $this->assertEquals($this->_helper, $returned); + $this->assertEquals($this->_nav2, $returned->getContainer()); + } + + public function testNullingOutNavigation() + { + $this->_helper->setContainer(); + $this->assertEquals(0, count($this->_helper->getContainer())); + } + + public function testRenderSuppliedContainerWithoutInterfering() + { + $rendered1 = $this->_getExpected('sitemap/default1.xml'); + $rendered2 = $this->_getExpected('sitemap/default2.xml'); + + $expected = array( + 'registered' => $rendered1, + 'supplied' => $rendered2, + 'registered_again' => $rendered1 + ); + $actual = array( + 'registered' => $this->_helper->render(), + 'supplied' => $this->_helper->render($this->_nav2), + 'registered_again' => $this->_helper->render() + ); + + $this->assertEquals($expected, $actual); + } + + public function testUseAclRoles() + { + $acl = $this->_getAcl(); + $this->_helper->setAcl($acl['acl']); + $this->_helper->setRole($acl['role']); + + $expected = $this->_getExpected('sitemap/acl.xml'); + $this->assertEquals($expected, $this->_helper->render()); + } + + public function testUseAclButNoRole() + { + $acl = $this->_getAcl(); + $this->_helper->setAcl($acl['acl']); + $this->_helper->setRole(null); + + $expected = $this->_getExpected('sitemap/acl2.xml'); + $this->assertEquals($expected, $this->_helper->render()); + } + + public function testSettingMaxDepth() + { + $this->_helper->setMaxDepth(0); + + $expected = $this->_getExpected('sitemap/depth1.xml'); + $this->assertEquals($expected, $this->_helper->render()); + } + + public function testSettingMinDepth() + { + $this->_helper->setMinDepth(1); + + $expected = $this->_getExpected('sitemap/depth2.xml'); + $this->assertEquals($expected, $this->_helper->render()); + } + + public function testSettingBothDepths() + { + $this->_helper->setMinDepth(1)->setMaxDepth(2); + + $expected = $this->_getExpected('sitemap/depth3.xml'); + $this->assertEquals($expected, $this->_helper->render()); + } + + public function testDropXmlDeclaration() + { + $this->_helper->setUseXmlDeclaration(false); + + $expected = $this->_getExpected('sitemap/nodecl.xml'); + $this->assertEquals($expected, $this->_helper->render($this->_nav2)); + } + + public function testThrowExceptionOnInvalidLoc() + { + $this->markTestIncomplete('Zend\URI changes affect this test'); + $nav = clone $this->_nav2; + $nav->addPage(array('label' => 'Invalid', 'uri' => 'http://w.')); + + try { + $this->_helper->render($nav); + } catch (View\Exception\ExceptionInterface $e) { + $expected = sprintf( + 'Encountered an invalid URL for Sitemap XML: "%s"', + 'http://w.'); + $actual = $e->getMessage(); + $this->assertEquals($expected, $actual); + return; + } + + $this->fail('A Zend_View_Exception was not thrown on invalid '); + } + + public function testDisablingValidators() + { + $nav = clone $this->_nav2; + $nav->addPage(array('label' => 'Invalid', 'uri' => 'http://w.')); + $this->_helper->setUseSitemapValidators(false); + + $expected = $this->_getExpected('sitemap/invalid.xml'); + $this->assertEquals($expected, $this->_helper->render($nav)); + } + + public function testSetServerUrlRequiresValidUri() + { + $this->markTestIncomplete('Zend\URI changes affect this test'); + try { + $this->_helper->setServerUrl('site.example.org'); + $this->fail('An invalid server URL was given, but a ' . + 'Zend\URI\Exception\ExceptionInterface was not thrown'); + } catch (\Zend\URI\Exception\ExceptionInterface $e) { + $this->assertContains('Illegal scheme', $e->getMessage()); + } + } + + public function testSetServerUrlWithSchemeAndHost() + { + $this->_helper->setServerUrl('http://sub.example.org'); + + $expected = $this->_getExpected('sitemap/serverurl1.xml'); + $this->assertEquals($expected, $this->_helper->render()); + } + + public function testSetServerUrlWithSchemeAndPortAndHostAndPath() + { + $this->_helper->setServerUrl('http://sub.example.org:8080/foo/'); + + $expected = $this->_getExpected('sitemap/serverurl2.xml'); + $this->assertEquals($expected, $this->_helper->render()); + } + + public function testGetUserSchemaValidation() + { + $this->_helper->setUseSchemaValidation(true); + $this->assertTrue($this->_helper->getUseSchemaValidation()); + $this->_helper->setUseSchemaValidation(false); + $this->assertFalse($this->_helper->getUseSchemaValidation()); + } + + public function testUseSchemaValidation() + { + $this->markTestSkipped('Skipped because it fetches XSD from web'); + return; + $nav = clone $this->_nav2; + $this->_helper->setUseSitemapValidators(false); + $this->_helper->setUseSchemaValidation(true); + $nav->addPage(array('label' => 'Invalid', 'uri' => 'http://w.')); + + try { + $this->_helper->render($nav); + } catch (View\Exception\ExceptionInterface $e) { + $expected = sprintf( + 'Sitemap is invalid according to XML Schema at "%s"', + \Zend\View\Helper\Navigation\Sitemap::SITEMAP_XSD); + $actual = $e->getMessage(); + $this->assertEquals($expected, $actual); + return; + } + + $this->fail('A Zend_View_Exception was not thrown when using Schema validation'); + } +} diff --git a/test/Helper/Navigation/_files/expected/bc/acl.html b/test/Helper/Navigation/_files/expected/bc/acl.html new file mode 100644 index 00000000..e2917ccf --- /dev/null +++ b/test/Helper/Navigation/_files/expected/bc/acl.html @@ -0,0 +1 @@ +Page 2 > Page 2.2 > Page 2.2.2 \ No newline at end of file diff --git a/test/Helper/Navigation/_files/expected/bc/default.html b/test/Helper/Navigation/_files/expected/bc/default.html new file mode 100644 index 00000000..91a252f2 --- /dev/null +++ b/test/Helper/Navigation/_files/expected/bc/default.html @@ -0,0 +1 @@ +Page 2 > Page 2.3 > Page 2.3.3 > Page 2.3.3.1 \ No newline at end of file diff --git a/test/Helper/Navigation/_files/expected/bc/linklast.html b/test/Helper/Navigation/_files/expected/bc/linklast.html new file mode 100644 index 00000000..71ef1cd7 --- /dev/null +++ b/test/Helper/Navigation/_files/expected/bc/linklast.html @@ -0,0 +1 @@ +Page 2 > Page 2.3 > Page 2.3.3 > Page 2.3.3.1 \ No newline at end of file diff --git a/test/Helper/Navigation/_files/expected/bc/maxdepth.html b/test/Helper/Navigation/_files/expected/bc/maxdepth.html new file mode 100644 index 00000000..c1fa8c91 --- /dev/null +++ b/test/Helper/Navigation/_files/expected/bc/maxdepth.html @@ -0,0 +1 @@ +Page 2 > Page 2.3 \ No newline at end of file diff --git a/test/Helper/Navigation/_files/expected/bc/partial.html b/test/Helper/Navigation/_files/expected/bc/partial.html new file mode 100644 index 00000000..bb7f30e1 --- /dev/null +++ b/test/Helper/Navigation/_files/expected/bc/partial.html @@ -0,0 +1 @@ +Page 2, Page 2.3, Page 2.3.3, Page 2.3.3.1 \ No newline at end of file diff --git a/test/Helper/Navigation/_files/expected/bc/separator.html b/test/Helper/Navigation/_files/expected/bc/separator.html new file mode 100644 index 00000000..7819eaff --- /dev/null +++ b/test/Helper/Navigation/_files/expected/bc/separator.html @@ -0,0 +1 @@ +Page 2fooPage 2.3fooPage 2.3.3fooPage 2.3.3.1 \ No newline at end of file diff --git a/test/Helper/Navigation/_files/expected/bc/translated.html b/test/Helper/Navigation/_files/expected/bc/translated.html new file mode 100644 index 00000000..3852c30f --- /dev/null +++ b/test/Helper/Navigation/_files/expected/bc/translated.html @@ -0,0 +1 @@ +Side 2 > Side 2.3 > Page 2.3.3 > Side 2.3.3.1 \ No newline at end of file diff --git a/test/Helper/Navigation/_files/expected/links/default.html b/test/Helper/Navigation/_files/expected/links/default.html new file mode 100644 index 00000000..3f3a9bc8 --- /dev/null +++ b/test/Helper/Navigation/_files/expected/links/default.html @@ -0,0 +1,7 @@ + + + + + + + \ No newline at end of file diff --git a/test/Helper/Navigation/_files/expected/menu/acl.html b/test/Helper/Navigation/_files/expected/menu/acl.html new file mode 100644 index 00000000..65d3fb4e --- /dev/null +++ b/test/Helper/Navigation/_files/expected/menu/acl.html @@ -0,0 +1,65 @@ + \ No newline at end of file diff --git a/test/Helper/Navigation/_files/expected/menu/acl_role_interface.html b/test/Helper/Navigation/_files/expected/menu/acl_role_interface.html new file mode 100644 index 00000000..7c09edd8 --- /dev/null +++ b/test/Helper/Navigation/_files/expected/menu/acl_role_interface.html @@ -0,0 +1,62 @@ + \ No newline at end of file diff --git a/test/Helper/Navigation/_files/expected/menu/acl_string.html b/test/Helper/Navigation/_files/expected/menu/acl_string.html new file mode 100644 index 00000000..7c09edd8 --- /dev/null +++ b/test/Helper/Navigation/_files/expected/menu/acl_string.html @@ -0,0 +1,62 @@ + \ No newline at end of file diff --git a/test/Helper/Navigation/_files/expected/menu/bothdepts.html b/test/Helper/Navigation/_files/expected/menu/bothdepts.html new file mode 100644 index 00000000..b3de7440 --- /dev/null +++ b/test/Helper/Navigation/_files/expected/menu/bothdepts.html @@ -0,0 +1,52 @@ + \ No newline at end of file diff --git a/test/Helper/Navigation/_files/expected/menu/css.html b/test/Helper/Navigation/_files/expected/menu/css.html new file mode 100644 index 00000000..a2680b66 --- /dev/null +++ b/test/Helper/Navigation/_files/expected/menu/css.html @@ -0,0 +1,11 @@ + \ No newline at end of file diff --git a/test/Helper/Navigation/_files/expected/menu/default1.html b/test/Helper/Navigation/_files/expected/menu/default1.html new file mode 100644 index 00000000..3d2854f5 --- /dev/null +++ b/test/Helper/Navigation/_files/expected/menu/default1.html @@ -0,0 +1,81 @@ + \ No newline at end of file diff --git a/test/Helper/Navigation/_files/expected/menu/default2.html b/test/Helper/Navigation/_files/expected/menu/default2.html new file mode 100644 index 00000000..c58f02b9 --- /dev/null +++ b/test/Helper/Navigation/_files/expected/menu/default2.html @@ -0,0 +1,11 @@ + \ No newline at end of file diff --git a/test/Helper/Navigation/_files/expected/menu/escapelabels_as_false.html b/test/Helper/Navigation/_files/expected/menu/escapelabels_as_false.html new file mode 100644 index 00000000..b6d17018 --- /dev/null +++ b/test/Helper/Navigation/_files/expected/menu/escapelabels_as_false.html @@ -0,0 +1,14 @@ + \ No newline at end of file diff --git a/test/Helper/Navigation/_files/expected/menu/escapelabels_as_true.html b/test/Helper/Navigation/_files/expected/menu/escapelabels_as_true.html new file mode 100644 index 00000000..59a633ca --- /dev/null +++ b/test/Helper/Navigation/_files/expected/menu/escapelabels_as_true.html @@ -0,0 +1,14 @@ + \ No newline at end of file diff --git a/test/Helper/Navigation/_files/expected/menu/indent4.html b/test/Helper/Navigation/_files/expected/menu/indent4.html new file mode 100644 index 00000000..06225471 --- /dev/null +++ b/test/Helper/Navigation/_files/expected/menu/indent4.html @@ -0,0 +1,81 @@ + \ No newline at end of file diff --git a/test/Helper/Navigation/_files/expected/menu/indent8.html b/test/Helper/Navigation/_files/expected/menu/indent8.html new file mode 100644 index 00000000..0ca545ec --- /dev/null +++ b/test/Helper/Navigation/_files/expected/menu/indent8.html @@ -0,0 +1,81 @@ + \ No newline at end of file diff --git a/test/Helper/Navigation/_files/expected/menu/maxdepth.html b/test/Helper/Navigation/_files/expected/menu/maxdepth.html new file mode 100644 index 00000000..4340f0b0 --- /dev/null +++ b/test/Helper/Navigation/_files/expected/menu/maxdepth.html @@ -0,0 +1,44 @@ + \ No newline at end of file diff --git a/test/Helper/Navigation/_files/expected/menu/mindepth.html b/test/Helper/Navigation/_files/expected/menu/mindepth.html new file mode 100644 index 00000000..cc88c82a --- /dev/null +++ b/test/Helper/Navigation/_files/expected/menu/mindepth.html @@ -0,0 +1,60 @@ + \ No newline at end of file diff --git a/test/Helper/Navigation/_files/expected/menu/onlyactivebranch.html b/test/Helper/Navigation/_files/expected/menu/onlyactivebranch.html new file mode 100644 index 00000000..e5cc77a8 --- /dev/null +++ b/test/Helper/Navigation/_files/expected/menu/onlyactivebranch.html @@ -0,0 +1,31 @@ + \ No newline at end of file diff --git a/test/Helper/Navigation/_files/expected/menu/onlyactivebranch_bothdepts.html b/test/Helper/Navigation/_files/expected/menu/onlyactivebranch_bothdepts.html new file mode 100644 index 00000000..326c25c0 --- /dev/null +++ b/test/Helper/Navigation/_files/expected/menu/onlyactivebranch_bothdepts.html @@ -0,0 +1,21 @@ + \ No newline at end of file diff --git a/test/Helper/Navigation/_files/expected/menu/onlyactivebranch_maxdepth.html b/test/Helper/Navigation/_files/expected/menu/onlyactivebranch_maxdepth.html new file mode 100644 index 00000000..9aa4869d --- /dev/null +++ b/test/Helper/Navigation/_files/expected/menu/onlyactivebranch_maxdepth.html @@ -0,0 +1,26 @@ + \ No newline at end of file diff --git a/test/Helper/Navigation/_files/expected/menu/onlyactivebranch_mindepth.html b/test/Helper/Navigation/_files/expected/menu/onlyactivebranch_mindepth.html new file mode 100644 index 00000000..41ab43cc --- /dev/null +++ b/test/Helper/Navigation/_files/expected/menu/onlyactivebranch_mindepth.html @@ -0,0 +1,26 @@ + \ No newline at end of file diff --git a/test/Helper/Navigation/_files/expected/menu/onlyactivebranch_noparents.html b/test/Helper/Navigation/_files/expected/menu/onlyactivebranch_noparents.html new file mode 100644 index 00000000..26ce1507 --- /dev/null +++ b/test/Helper/Navigation/_files/expected/menu/onlyactivebranch_noparents.html @@ -0,0 +1,8 @@ + \ No newline at end of file diff --git a/test/Helper/Navigation/_files/expected/menu/onlyactivebranch_np_bd.html b/test/Helper/Navigation/_files/expected/menu/onlyactivebranch_np_bd.html new file mode 100644 index 00000000..75797b82 --- /dev/null +++ b/test/Helper/Navigation/_files/expected/menu/onlyactivebranch_np_bd.html @@ -0,0 +1,8 @@ + \ No newline at end of file diff --git a/test/Helper/Navigation/_files/expected/menu/onlyactivebranch_np_bd2.html b/test/Helper/Navigation/_files/expected/menu/onlyactivebranch_np_bd2.html new file mode 100644 index 00000000..c5f1eeb6 --- /dev/null +++ b/test/Helper/Navigation/_files/expected/menu/onlyactivebranch_np_bd2.html @@ -0,0 +1,11 @@ + \ No newline at end of file diff --git a/test/Helper/Navigation/_files/expected/menu/partial.html b/test/Helper/Navigation/_files/expected/menu/partial.html new file mode 100644 index 00000000..356c5a19 --- /dev/null +++ b/test/Helper/Navigation/_files/expected/menu/partial.html @@ -0,0 +1,2 @@ +Is a container: yes +Pages: Home, Page 1, Page 2, Page 3, Zym \ No newline at end of file diff --git a/test/Helper/Navigation/_files/expected/menu/translated.html b/test/Helper/Navigation/_files/expected/menu/translated.html new file mode 100644 index 00000000..f85661e6 --- /dev/null +++ b/test/Helper/Navigation/_files/expected/menu/translated.html @@ -0,0 +1,81 @@ + \ No newline at end of file diff --git a/test/Helper/Navigation/_files/expected/sitemap/acl.xml b/test/Helper/Navigation/_files/expected/sitemap/acl.xml new file mode 100644 index 00000000..3b7319e8 --- /dev/null +++ b/test/Helper/Navigation/_files/expected/sitemap/acl.xml @@ -0,0 +1,54 @@ + + + + http://localhost/index + + + http://localhost/page1 + + + http://localhost/page1/page1_1 + + + http://localhost/page2 + + + http://localhost/page2/page2_1 + + + http://localhost/page2/page2_2 + + + http://localhost/page2/page2_2/page2_2_1 + + + http://localhost/page2/page2_2/page2_2_2 + + + http://localhost/page2/page2_3 + + + http://localhost/page2/page2_3/page2_3_1 + + + http://localhost/page3 + + + http://localhost/page3/page3_1 + + + http://localhost/page3/page3_2 + + + http://localhost/page3/page3_2/page3_2_1 + + + http://localhost/page3/page3_2/page3_2_2 + + + http://localhost/page3/page3_3 + + + http://www.zym-project.com/ + + \ No newline at end of file diff --git a/test/Helper/Navigation/_files/expected/sitemap/acl2.xml b/test/Helper/Navigation/_files/expected/sitemap/acl2.xml new file mode 100644 index 00000000..c708eeb9 --- /dev/null +++ b/test/Helper/Navigation/_files/expected/sitemap/acl2.xml @@ -0,0 +1,39 @@ + + + + http://localhost/index + + + http://localhost/page1 + + + http://localhost/page1/page1_1 + + + http://localhost/page2 + + + http://localhost/page2/page2_1 + + + http://localhost/page2/page2_2 + + + http://localhost/page2/page2_2/page2_2_1 + + + http://localhost/page2/page2_2/page2_2_2 + + + http://localhost/page2/page2_3 + + + http://localhost/page2/page2_3/page2_3_1 + + + http://localhost/page3 + + + http://www.zym-project.com/ + + \ No newline at end of file diff --git a/test/Helper/Navigation/_files/expected/sitemap/default1.xml b/test/Helper/Navigation/_files/expected/sitemap/default1.xml new file mode 100644 index 00000000..905beb1e --- /dev/null +++ b/test/Helper/Navigation/_files/expected/sitemap/default1.xml @@ -0,0 +1,66 @@ + + + + http://localhost/index + + + http://localhost/page1 + + + http://localhost/page1/page1_1 + + + http://localhost/page2 + + + http://localhost/page2/page2_1 + + + http://localhost/page2/page2_2 + + + http://localhost/page2/page2_2/page2_2_1 + + + http://localhost/page2/page2_2/page2_2_2 + + + http://localhost/page2/page2_3 + + + http://localhost/page2/page2_3/page2_3_1 + + + http://localhost/page2/page2_3/page2_3_3 + + + http://localhost/page2/page2_3/page2_3_3/1 + + + http://localhost/page2/page2_3/page2_3_3/2 + + + http://localhost/page3 + + + http://localhost/page3/page3_1 + + + http://localhost/page3/page3_2 + + + http://localhost/page3/page3_2/page3_2_1 + + + http://localhost/page3/page3_2/page3_2_2 + + + http://localhost/page3/page3_3 + + + http://localhost/page3/page3_3/page3_3_2 + + + http://www.zym-project.com/ + + \ No newline at end of file diff --git a/test/Helper/Navigation/_files/expected/sitemap/default2.xml b/test/Helper/Navigation/_files/expected/sitemap/default2.xml new file mode 100644 index 00000000..012f5c50 --- /dev/null +++ b/test/Helper/Navigation/_files/expected/sitemap/default2.xml @@ -0,0 +1,14 @@ + + + + http://localhost/site1 + daily + 0.9 + + + http://localhost/site2 + + + http://localhost/site3 + + \ No newline at end of file diff --git a/test/Helper/Navigation/_files/expected/sitemap/depth1.xml b/test/Helper/Navigation/_files/expected/sitemap/depth1.xml new file mode 100644 index 00000000..503800c6 --- /dev/null +++ b/test/Helper/Navigation/_files/expected/sitemap/depth1.xml @@ -0,0 +1,18 @@ + + + + http://localhost/index + + + http://localhost/page1 + + + http://localhost/page2 + + + http://localhost/page3 + + + http://www.zym-project.com/ + + \ No newline at end of file diff --git a/test/Helper/Navigation/_files/expected/sitemap/depth2.xml b/test/Helper/Navigation/_files/expected/sitemap/depth2.xml new file mode 100644 index 00000000..cf610e55 --- /dev/null +++ b/test/Helper/Navigation/_files/expected/sitemap/depth2.xml @@ -0,0 +1,51 @@ + + + + http://localhost/page1/page1_1 + + + http://localhost/page2/page2_1 + + + http://localhost/page2/page2_2 + + + http://localhost/page2/page2_2/page2_2_1 + + + http://localhost/page2/page2_2/page2_2_2 + + + http://localhost/page2/page2_3 + + + http://localhost/page2/page2_3/page2_3_1 + + + http://localhost/page2/page2_3/page2_3_3 + + + http://localhost/page2/page2_3/page2_3_3/1 + + + http://localhost/page2/page2_3/page2_3_3/2 + + + http://localhost/page3/page3_1 + + + http://localhost/page3/page3_2 + + + http://localhost/page3/page3_2/page3_2_1 + + + http://localhost/page3/page3_2/page3_2_2 + + + http://localhost/page3/page3_3 + + + http://localhost/page3/page3_3/page3_3_2 + + \ No newline at end of file diff --git a/test/Helper/Navigation/_files/expected/sitemap/depth3.xml b/test/Helper/Navigation/_files/expected/sitemap/depth3.xml new file mode 100644 index 00000000..55cabea1 --- /dev/null +++ b/test/Helper/Navigation/_files/expected/sitemap/depth3.xml @@ -0,0 +1,45 @@ + + + + http://localhost/page1/page1_1 + + + http://localhost/page2/page2_1 + + + http://localhost/page2/page2_2 + + + http://localhost/page2/page2_2/page2_2_1 + + + http://localhost/page2/page2_2/page2_2_2 + + + http://localhost/page2/page2_3 + + + http://localhost/page2/page2_3/page2_3_1 + + + http://localhost/page2/page2_3/page2_3_3 + + + http://localhost/page3/page3_1 + + + http://localhost/page3/page3_2 + + + http://localhost/page3/page3_2/page3_2_1 + + + http://localhost/page3/page3_2/page3_2_2 + + + http://localhost/page3/page3_3 + + + http://localhost/page3/page3_3/page3_3_2 + + \ No newline at end of file diff --git a/test/Helper/Navigation/_files/expected/sitemap/invalid.xml b/test/Helper/Navigation/_files/expected/sitemap/invalid.xml new file mode 100644 index 00000000..940e92bb --- /dev/null +++ b/test/Helper/Navigation/_files/expected/sitemap/invalid.xml @@ -0,0 +1,19 @@ + + + + http://localhost/site1 + daily + 0.9 + + + http://localhost/site2 + + + + http://localhost/site3 + often + + + http://w. + + \ No newline at end of file diff --git a/test/Helper/Navigation/_files/expected/sitemap/nodecl.xml b/test/Helper/Navigation/_files/expected/sitemap/nodecl.xml new file mode 100644 index 00000000..0ab0e17e --- /dev/null +++ b/test/Helper/Navigation/_files/expected/sitemap/nodecl.xml @@ -0,0 +1,13 @@ + + + http://localhost/site1 + daily + 0.9 + + + http://localhost/site2 + + + http://localhost/site3 + + \ No newline at end of file diff --git a/test/Helper/Navigation/_files/expected/sitemap/serverurl1.xml b/test/Helper/Navigation/_files/expected/sitemap/serverurl1.xml new file mode 100644 index 00000000..9804b2a2 --- /dev/null +++ b/test/Helper/Navigation/_files/expected/sitemap/serverurl1.xml @@ -0,0 +1,66 @@ + + + + http://sub.example.org/index + + + http://sub.example.org/page1 + + + http://sub.example.org/page1/page1_1 + + + http://sub.example.org/page2 + + + http://sub.example.org/page2/page2_1 + + + http://sub.example.org/page2/page2_2 + + + http://sub.example.org/page2/page2_2/page2_2_1 + + + http://sub.example.org/page2/page2_2/page2_2_2 + + + http://sub.example.org/page2/page2_3 + + + http://sub.example.org/page2/page2_3/page2_3_1 + + + http://sub.example.org/page2/page2_3/page2_3_3 + + + http://sub.example.org/page2/page2_3/page2_3_3/1 + + + http://sub.example.org/page2/page2_3/page2_3_3/2 + + + http://sub.example.org/page3 + + + http://sub.example.org/page3/page3_1 + + + http://sub.example.org/page3/page3_2 + + + http://sub.example.org/page3/page3_2/page3_2_1 + + + http://sub.example.org/page3/page3_2/page3_2_2 + + + http://sub.example.org/page3/page3_3 + + + http://sub.example.org/page3/page3_3/page3_3_2 + + + http://www.zym-project.com/ + + \ No newline at end of file diff --git a/test/Helper/Navigation/_files/expected/sitemap/serverurl2.xml b/test/Helper/Navigation/_files/expected/sitemap/serverurl2.xml new file mode 100644 index 00000000..ae4b31f4 --- /dev/null +++ b/test/Helper/Navigation/_files/expected/sitemap/serverurl2.xml @@ -0,0 +1,66 @@ + + + + http://sub.example.org:8080/index + + + http://sub.example.org:8080/page1 + + + http://sub.example.org:8080/page1/page1_1 + + + http://sub.example.org:8080/page2 + + + http://sub.example.org:8080/page2/page2_1 + + + http://sub.example.org:8080/page2/page2_2 + + + http://sub.example.org:8080/page2/page2_2/page2_2_1 + + + http://sub.example.org:8080/page2/page2_2/page2_2_2 + + + http://sub.example.org:8080/page2/page2_3 + + + http://sub.example.org:8080/page2/page2_3/page2_3_1 + + + http://sub.example.org:8080/page2/page2_3/page2_3_3 + + + http://sub.example.org:8080/page2/page2_3/page2_3_3/1 + + + http://sub.example.org:8080/page2/page2_3/page2_3_3/2 + + + http://sub.example.org:8080/page3 + + + http://sub.example.org:8080/page3/page3_1 + + + http://sub.example.org:8080/page3/page3_2 + + + http://sub.example.org:8080/page3/page3_2/page3_2_1 + + + http://sub.example.org:8080/page3/page3_2/page3_2_2 + + + http://sub.example.org:8080/page3/page3_3 + + + http://sub.example.org:8080/page3/page3_3/page3_3_2 + + + http://www.zym-project.com/ + + \ No newline at end of file diff --git a/test/Helper/Navigation/_files/mvc/views/bc.phtml b/test/Helper/Navigation/_files/mvc/views/bc.phtml new file mode 100644 index 00000000..082a6080 --- /dev/null +++ b/test/Helper/Navigation/_files/mvc/views/bc.phtml @@ -0,0 +1,4 @@ +getLabel();'), + $this->vars()->pages)); diff --git a/test/Helper/Navigation/_files/mvc/views/menu.phtml b/test/Helper/Navigation/_files/mvc/views/menu.phtml new file mode 100644 index 00000000..004ee04f --- /dev/null +++ b/test/Helper/Navigation/_files/mvc/views/menu.phtml @@ -0,0 +1,13 @@ +vars('container') instanceof \Zend\Navigation\AbstractContainer + ? 'yes' + : 'no'; + +$pages = array(); +foreach ($this->vars('container') as $page) { + $pages[] = $page->getLabel(); +} +$pages = implode(', ', $pages); +?> +Is a container: +Pages: diff --git a/test/Helper/Navigation/_files/navigation.xml b/test/Helper/Navigation/_files/navigation.xml new file mode 100644 index 00000000..caed03f9 --- /dev/null +++ b/test/Helper/Navigation/_files/navigation.xml @@ -0,0 +1,212 @@ + + + + + + + + http://www.zym-project.com/ + 100 + + + + + page1 + + + + + page1/page1_1 + + + + + + + + page2 + + + + + page2/page2_1 + + + + + page2/page2_2 + + + + + page2/page2_2/page2_2_1 + + + + + page2/page2_2/page2_2_2 + 1 + + + + + + + + page2/page2_3 + + + + + page2/page2_3/page2_3_1 + + + + + page2/page2_3/page2_3_2 + 0 + + + + + page2/page2_3/page2_3_2/1 + 1 + + + + + page2/page2_3/page2_3_2/2 + 1 + + + + + # + 1 + + + + + + + + + + page2/page2_3/page2_3_3 + admin_foo + + + + + page2/page2_3/page2_3_3/1 + 1 + + + + + page2/page2_3/page2_3_3/2 + guest_foo + 1 + + + + + + + + + + + + + + page3 + + + + + page3/page3_1 + guest_foo + + + + + page3/page3_2 + member_foo + + + + + page3/page3_2/page3_2_1 + + + + + page3/page3_2/page3_2_2 + admin_foo + read + + + + + + + + page3/page3_3 + special_foo + + + + + page3/page3_3/page3_3_1 + 0 + + + + + page3/page3_3/page3_3_2 + admin_foo + + + + + + + + + + + index + Go home + -100 + + + + + + + + + + + site1 + daily + 0.9 + + + + + site2 + 1 + earlier + + + + + site3 + often + + + + + \ No newline at end of file diff --git a/test/Helper/PaginationControlTest.php b/test/Helper/PaginationControlTest.php new file mode 100644 index 00000000..9ca7217e --- /dev/null +++ b/test/Helper/PaginationControlTest.php @@ -0,0 +1,182 @@ + array( + __DIR__ . '/_files/scripts', + ))); + $view = new View(); + $view->setResolver($resolver); + + Helper\PaginationControl::setDefaultViewPartial(null); + $this->_viewHelper = new Helper\PaginationControl(); + $this->_viewHelper->setView($view); + $adapter = new Paginator\Adapter\ArrayAdapter(range(1, 101)); + $this->_paginator = new Paginator\Paginator($adapter); + } + + public function tearDown() + { + unset($this->_viewHelper); + unset($this->_paginator); + } + + public function testGetsAndSetsView() + { + $view = new View(); + $helper = new Helper\PaginationControl(); + $this->assertNull($helper->getView()); + $helper->setView($view); + $this->assertInstanceOf('Zend\View\Renderer\RendererInterface', $helper->getView()); + } + + public function testGetsAndSetsDefaultViewPartial() + { + $this->assertNull(Helper\PaginationControl::getDefaultViewPartial()); + Helper\PaginationControl::setDefaultViewPartial('partial'); + $this->assertEquals('partial', Helper\PaginationControl::getDefaultViewPartial()); + Helper\PaginationControl::setDefaultViewPartial(null); + } + + public function testUsesDefaultViewPartialIfNoneSupplied() + { + Helper\PaginationControl::setDefaultViewPartial('testPagination.phtml'); + $output = $this->_viewHelper->__invoke($this->_paginator); + $this->assertContains('pagination control', $output, $output); + Helper\PaginationControl::setDefaultViewPartial(null); + } + + public function testThrowsExceptionIfNoViewPartialFound() + { + try { + $this->_viewHelper->__invoke($this->_paginator); + } catch (\Exception $e) { + $this->assertInstanceOf('Zend\View\Exception\ExceptionInterface', $e); + $this->assertEquals('No view partial provided and no default set', $e->getMessage()); + } + } + + /** + * @group ZF-4037 + */ + public function testUsesDefaultScrollingStyleIfNoneSupplied() + { + // First we'll make sure the base case works + $output = $this->_viewHelper->__invoke($this->_paginator, 'All', 'testPagination.phtml'); + $this->assertContains('page count (11) equals pages in range (11)', $output, $output); + + Paginator\Paginator::setDefaultScrollingStyle('All'); + $output = $this->_viewHelper->__invoke($this->_paginator, null, 'testPagination.phtml'); + $this->assertContains('page count (11) equals pages in range (11)', $output, $output); + + Helper\PaginationControl::setDefaultViewPartial('testPagination.phtml'); + $output = $this->_viewHelper->__invoke($this->_paginator); + $this->assertContains('page count (11) equals pages in range (11)', $output, $output); + } + + /** + * @group ZF-4153 + */ + public function testUsesPaginatorFromViewIfNoneSupplied() + { + $this->_viewHelper->getView()->paginator = $this->_paginator; + Helper\PaginationControl::setDefaultViewPartial('testPagination.phtml'); + + $output = $this->_viewHelper->__invoke(); + + $this->assertContains('pagination control', $output, $output); + } + + /** + * @group ZF-4153 + */ + public function testThrowsExceptionIfNoPaginatorFound() + { + Helper\PaginationControl::setDefaultViewPartial('testPagination.phtml'); + + $this->setExpectedException( + 'Zend\View\Exception\ExceptionInterface', + 'No paginator instance provided or incorrect type' + ); + $this->_viewHelper->__invoke(); + } + + /** + * @group ZF-4233 + */ + public function testAcceptsViewPartialInOtherModule() + { + try { + $this->_viewHelper->__invoke($this->_paginator, null, array('partial.phtml', 'test')); + } catch (\Exception $e) { + /* We don't care whether or not the module exists--we just want to + * make sure it gets to Zend_View_Helper_Partial and it's recognized + * as a module. */ + $this->assertInstanceOf('Zend\View\Exception\RuntimeException', $e); + $this->assertContains('could not resolve', $e->getMessage()); + } + } + + /** + * @group ZF-4328 + */ + public function testUsesPaginatorFromViewOnlyIfNoneSupplied() + { + $this->_viewHelper->getView()->vars()->paginator = $this->_paginator; + $paginator = new Paginator\Paginator(new Paginator\Adapter\ArrayAdapter(range(1, 30))); + Helper\PaginationControl::setDefaultViewPartial('testPagination.phtml'); + + $output = $this->_viewHelper->__invoke($paginator); + $this->assertContains('page count (3)', $output, $output); + } + + /** + * @group ZF-4878 + */ + public function testCanUseObjectForScrollingStyle() + { + $all = new Paginator\ScrollingStyle\All(); + + $output = $this->_viewHelper->__invoke($this->_paginator, $all, 'testPagination.phtml'); + + $this->assertContains('page count (11) equals pages in range (11)', $output, $output); + } +} + diff --git a/test/Helper/PartialLoopTest.php b/test/Helper/PartialLoopTest.php new file mode 100644 index 00000000..f976419c --- /dev/null +++ b/test/Helper/PartialLoopTest.php @@ -0,0 +1,457 @@ +basePath = __DIR__ . '/_files/modules'; + $this->helper = new PartialLoop(); + } + + /** + * Tears down the fixture, for example, close a network connection. + * This method is called after a test is executed. + * + * @return void + */ + public function tearDown() + { + unset($this->helper); + } + + /** + * @return void + */ + public function testPartialLoopIteratesOverArray() + { + $data = array( + array('message' => 'foo'), + array('message' => 'bar'), + array('message' => 'baz'), + array('message' => 'bat'), + ); + + $view = new View(); + $view->resolver()->addPath($this->basePath . '/application/views/scripts'); + $this->helper->setView($view); + + $result = $this->helper->__invoke('partialLoop.phtml', $data); + foreach ($data as $item) { + $string = 'This is an iteration: ' . $item['message']; + $this->assertContains($string, $result); + } + } + + /** + * @return void + */ + public function testPartialLoopIteratesOverIterator() + { + $data = array( + array('message' => 'foo'), + array('message' => 'bar'), + array('message' => 'baz'), + array('message' => 'bat') + ); + $o = new IteratorTest($data); + + $view = new View(); + $view->resolver()->addPath($this->basePath . '/application/views/scripts'); + $this->helper->setView($view); + + $result = $this->helper->__invoke('partialLoop.phtml', $o); + foreach ($data as $item) { + $string = 'This is an iteration: ' . $item['message']; + $this->assertContains($string, $result); + } + } + + /** + * @return void + */ + public function testPartialLoopIteratesOverRecursiveIterator() + { + $rIterator = new RecursiveIteratorTest(); + for ($i = 0; $i < 5; ++$i) { + $data = array( + 'message' => 'foo' . $i, + ); + $rIterator->addItem(new IteratorTest($data)); + } + + $view = new View(); + $view->resolver()->addPath($this->basePath . '/application/views/scripts'); + $this->helper->setView($view); + + $result = $this->helper->__invoke('partialLoop.phtml', $rIterator); + foreach ($rIterator as $item) { + foreach ($item as $key => $value) { + $this->assertContains($value, $result, var_export($value, 1)); + } + } + } + + /** + * @return void + */ + public function testPartialLoopThrowsExceptionWithBadIterator() + { + $data = array( + array('message' => 'foo'), + array('message' => 'bar'), + array('message' => 'baz'), + array('message' => 'bat') + ); + $o = new BogusIteratorTest($data); + + $view = new View(); + $view->resolver()->addPath($this->basePath . '/application/views/scripts'); + $this->helper->setView($view); + + try { + $result = $this->helper->__invoke('partialLoop.phtml', $o); + $this->fail('PartialLoop should only work with arrays and iterators'); + } catch (\Exception $e) { + } + } + + public function testPassingNoArgsReturnsHelperInstance() + { + $test = $this->helper->__invoke(); + $this->assertSame($this->helper, $test); + } + + public function testShouldAllowIteratingOverTraversableObjects() + { + $data = array( + array('message' => 'foo'), + array('message' => 'bar'), + array('message' => 'baz'), + array('message' => 'bat') + ); + $o = new ArrayObject($data); + + $view = new View(); + $view->resolver()->addPath($this->basePath . '/application/views/scripts'); + $this->helper->setView($view); + + $result = $this->helper->__invoke('partialLoop.phtml', $o); + foreach ($data as $item) { + $string = 'This is an iteration: ' . $item['message']; + $this->assertContains($string, $result); + } + } + + public function testShouldAllowIteratingOverObjectsImplementingToArray() + { + $data = array( + array('message' => 'foo'), + array('message' => 'bar'), + array('message' => 'baz'), + array('message' => 'bat') + ); + $o = new ToArrayTest($data); + + $view = new View(); + $view->resolver()->addPath($this->basePath . '/application/views/scripts'); + $this->helper->setView($view); + + $result = $this->helper->__invoke('partialLoop.phtml', $o); + foreach ($data as $item) { + $string = 'This is an iteration: ' . $item['message']; + $this->assertContains($string, $result, $result); + } + } + + /** + * @group ZF-3350 + * @group ZF-3352 + */ + public function testShouldNotCastToArrayIfObjectIsTraversable() + { + $data = array( + new IteratorWithToArrayTestContainer(array('message' => 'foo')), + new IteratorWithToArrayTestContainer(array('message' => 'bar')), + new IteratorWithToArrayTestContainer(array('message' => 'baz')), + new IteratorWithToArrayTestContainer(array('message' => 'bat')), + ); + $o = new IteratorWithToArrayTest($data); + + $view = new View(); + $view->resolver()->addPath($this->basePath . '/application/views/scripts'); + $this->helper->setView($view); + $this->helper->setObjectKey('obj'); + + $result = $this->helper->__invoke('partialLoopObject.phtml', $o); + foreach ($data as $item) { + $string = 'This is an iteration: ' . $item->message; + $this->assertContains($string, $result, $result); + } + } + + /** + * @group ZF-3083 + */ + public function testEmptyArrayPassedToPartialLoopShouldNotThrowException() + { + $view = new View(); + $view->resolver()->addPath($this->basePath . '/application/views/scripts'); + $this->helper->setView($view); + + $this->helper->__invoke('partialLoop.phtml', array()); + } + + /** + * @group ZF-2737 + */ + public function testPartialLoopIncramentsPartialCounter() + { + $data = array( + array('message' => 'foo'), + array('message' => 'bar'), + array('message' => 'baz'), + array('message' => 'bat') + ); + + $view = new View(); + $view->resolver()->addPath($this->basePath . '/application/views/scripts'); + $this->helper->setView($view); + + $result = $this->helper->__invoke('partialLoopCouter.phtml', $data); + foreach ($data as $key => $item) { + $string = sprintf( + 'This is an iteration: %s, pointer at %d', + $item['message'], + $key + 1 + ); + $this->assertContains($string, $result, $result); + } + } + + /** + * @group ZF-5174 + */ + public function testPartialLoopPartialCounterResets() + { + $data = array( + array('message' => 'foo'), + array('message' => 'bar'), + array('message' => 'baz'), + array('message' => 'bat') + ); + + $view = new View(); + $view->resolver()->addPath($this->basePath . '/application/views/scripts'); + $this->helper->setView($view); + + $result = $this->helper->__invoke('partialLoopCouter.phtml', $data); + foreach ($data as $key=>$item) { + $string = 'This is an iteration: ' . $item['message'] . ', pointer at ' . ($key+1); + $this->assertContains($string, $result); + } + + $result = $this->helper->__invoke('partialLoopCouter.phtml', $data); + foreach ($data as $key=>$item) { + $string = 'This is an iteration: ' . $item['message'] . ', pointer at ' . ($key+1); + $this->assertContains($string, $result); + } + } +} + +class IteratorTest implements Iterator +{ + public $items; + + public function __construct(array $array) + { + $this->items = $array; + } + + public function current() + { + return current($this->items); + } + + public function key() + { + return key($this->items); + } + + public function next() + { + return next($this->items); + } + + public function rewind() + { + return reset($this->items); + } + + public function valid() + { + return (current($this->items) !== false); + } + + public function toArray() + { + return $this->items; + } +} + +class RecursiveIteratorTest implements Iterator +{ + public $items; + + public function __construct() + { + $this->items = array(); + } + + public function addItem(Iterator $iterator) + { + $this->items[] = $iterator; + return $this; + } + + public function current() + { + return current($this->items); + } + + public function key() + { + return key($this->items); + } + + public function next() + { + return next($this->items); + } + + public function rewind() + { + return reset($this->items); + } + + public function valid() + { + return (current($this->items) !== false); + } +} + +class BogusIteratorTest +{ +} + +class ToArrayTest +{ + public function __construct(array $data) + { + $this->data = $data; + } + + public function toArray() + { + return $this->data; + } +} + +class IteratorWithToArrayTest implements Iterator +{ + public $items; + + public function __construct(array $array) + { + $this->items = $array; + } + + public function toArray() + { + return $this->items; + } + + public function current() + { + return current($this->items); + } + + public function key() + { + return key($this->items); + } + + public function next() + { + return next($this->items); + } + + public function rewind() + { + return reset($this->items); + } + + public function valid() + { + return (current($this->items) !== false); + } +} + +class IteratorWithToArrayTestContainer +{ + protected $_info; + + public function __construct(array $info) + { + foreach ($info as $key => $value) { + $this->$key = $value; + } + $this->_info = $info; + } + + public function toArray() + { + return $this->_info; + } +} + diff --git a/test/Helper/PartialTest.php b/test/Helper/PartialTest.php new file mode 100644 index 00000000..46d69973 --- /dev/null +++ b/test/Helper/PartialTest.php @@ -0,0 +1,220 @@ +basePath = __DIR__ . '/_files/modules'; + $this->helper = new Partial(); + } + + /** + * Tears down the fixture, for example, close a network connection. + * This method is called after a test is executed. + * + * @return void + */ + public function tearDown() + { + unset($this->helper); + } + + /** + * @return void + */ + public function testPartialRendersScript() + { + $view = new View(); + $view->resolver()->addPath($this->basePath . '/application/views/scripts'); + $this->helper->setView($view); + $return = $this->helper->__invoke('partialOne.phtml'); + $this->assertContains('This is the first test partial', $return); + } + + /** + * @return void + */ + public function testPartialRendersScriptWithVars() + { + $view = new View(); + $view->resolver()->addPath($this->basePath . '/application/views/scripts'); + $view->vars()->message = 'This should never be read'; + $this->helper->setView($view); + $return = $this->helper->__invoke('partialThree.phtml', array('message' => 'This message should be read')); + $this->assertNotContains('This should never be read', $return); + $this->assertContains('This message should be read', $return, $return); + } + + /** + * @return void + */ + public function testSetViewSetsViewProperty() + { + $view = new View(); + $this->helper->setView($view); + $this->assertSame($view, $this->helper->getView()); + } + + /** + * @return void + */ + public function testCloneViewReturnsDifferentViewInstance() + { + $view = new View(); + $this->helper->setView($view); + $clone = $this->helper->cloneView(); + $this->assertNotSame($view, $clone); + $this->assertTrue($clone instanceof View); + } + + /** + * @return void + */ + public function testCloneViewClearsViewVariables() + { + $view = new View(); + $view->foo = 'bar'; + $this->helper->setView($view); + + $clone = $this->helper->cloneView(); + $clonedVars = $clone->vars(); + + $this->assertEquals(0, count($clonedVars)); + $this->assertNull($clone->vars()->foo); + } + + public function testObjectModelWithPublicPropertiesSetsViewVariables() + { + $model = new \stdClass(); + $model->foo = 'bar'; + $model->bar = 'baz'; + + $view = new View(); + $view->resolver()->addPath($this->basePath . '/application/views/scripts'); + $this->helper->setView($view); + $return = $this->helper->__invoke('partialVars.phtml', $model); + + foreach (get_object_vars($model) as $key => $value) { + $string = sprintf('%s: %s', $key, $value); + $this->assertContains($string, $return); + } + } + + public function testObjectModelWithToArraySetsViewVariables() + { + $model = new Aggregate(); + + $view = new View(); + $view->resolver()->addPath($this->basePath . '/application/views/scripts'); + $this->helper->setView($view); + $return = $this->helper->__invoke('partialVars.phtml', $model); + + foreach ($model->toArray() as $key => $value) { + $string = sprintf('%s: %s', $key, $value); + $this->assertContains($string, $return); + } + } + + public function testObjectModelSetInObjectKeyWhenKeyPresent() + { + $this->helper->setObjectKey('foo'); + $model = new \stdClass(); + $model->footest = 'bar'; + $model->bartest = 'baz'; + + $view = new View; + $view->resolver()->addPath($this->basePath . '/application/views/scripts'); + $this->helper->setView($view); + $return = $this->helper->__invoke('partialObj.phtml', $model); + + $this->assertNotContains('No object model passed', $return); + + foreach (get_object_vars($model) as $key => $value) { + $string = sprintf('%s: %s', $key, $value); + $this->assertContains($string, $return, "Checking for '$return' containing '$string'"); + } + } + + public function testPassingNoArgsReturnsHelperInstance() + { + $test = $this->helper->__invoke(); + $this->assertSame($this->helper, $test); + } + + public function testObjectKeyIsNullByDefault() + { + $this->assertNull($this->helper->getObjectKey()); + } + + public function testCanSetObjectKey() + { + $this->testObjectKeyIsNullByDefault(); + $this->helper->setObjectKey('foo'); + $this->assertEquals('foo', $this->helper->getObjectKey()); + } + + public function testCanSetObjectKeyToNullValue() + { + $this->testCanSetObjectKey(); + $this->helper->setObjectKey(null); + $this->assertNull($this->helper->getObjectKey()); + } + + public function testSetObjectKeyImplementsFluentInterface() + { + $test = $this->helper->setObjectKey('foo'); + $this->assertSame($this->helper, $test); + } +} + +class Aggregate +{ + public $vars = array( + 'foo' => 'bar', + 'bar' => 'baz' + ); + + public function toArray() + { + return $this->vars; + } +} diff --git a/test/Helper/Placeholder/ContainerTest.php b/test/Helper/Placeholder/ContainerTest.php new file mode 100644 index 00000000..7381e7f6 --- /dev/null +++ b/test/Helper/Placeholder/ContainerTest.php @@ -0,0 +1,415 @@ +container = new \Zend\View\Helper\Placeholder\Container(array()); + } + + /** + * Tears down the fixture, for example, close a network connection. + * This method is called after a test is executed. + * + * @return void + */ + public function tearDown() + { + unset($this->container); + } + + /** + * @return void + */ + public function testSetSetsASingleValue() + { + $this->container['foo'] = 'bar'; + $this->container['bar'] = 'baz'; + $this->assertEquals('bar', $this->container['foo']); + $this->assertEquals('baz', $this->container['bar']); + + $this->container->set('foo'); + $this->assertEquals(1, count($this->container)); + $this->assertEquals('foo', $this->container[0]); + } + + /** + * @return void + */ + public function testGetValueReturnsScalarWhenOneElementRegistered() + { + $this->container->set('foo'); + $this->assertEquals('foo', $this->container->getValue()); + } + + /** + * @return void + */ + public function testGetValueReturnsArrayWhenMultipleValuesPresent() + { + $this->container['foo'] = 'bar'; + $this->container['bar'] = 'baz'; + $expected = array('foo' => 'bar', 'bar' => 'baz'); + $return = $this->container->getValue(); + $this->assertEquals($expected, $return); + } + + /** + * @return void + */ + public function testPrefixAccesorsWork() + { + $this->assertEquals('', $this->container->getPrefix()); + $this->container->setPrefix('
      • '); + $this->assertEquals('
        • ', $this->container->getPrefix()); + } + + /** + * @return void + */ + public function testSetPrefixImplementsFluentInterface() + { + $result = $this->container->setPrefix('
          • '); + $this->assertSame($this->container, $result); + } + + /** + * @return void + */ + public function testPostfixAccesorsWork() + { + $this->assertEquals('', $this->container->getPostfix()); + $this->container->setPostfix('
          '); + $this->assertEquals('
        ', $this->container->getPostfix()); + } + + /** + * @return void + */ + public function testSetPostfixImplementsFluentInterface() + { + $result = $this->container->setPostfix('
      '); + $this->assertSame($this->container, $result); + } + + /** + * @return void + */ + public function testSeparatorAccesorsWork() + { + $this->assertEquals('', $this->container->getSeparator()); + $this->container->setSeparator('
    • '); + $this->assertEquals('
    • ', $this->container->getSeparator()); + } + + /** + * @return void + */ + public function testSetSeparatorImplementsFluentInterface() + { + $result = $this->container->setSeparator('
    • '); + $this->assertSame($this->container, $result); + } + + /** + * @return void + */ + public function testIndentAccesorsWork() + { + $this->assertEquals('', $this->container->getIndent()); + $this->container->setIndent(' '); + $this->assertEquals(' ', $this->container->getIndent()); + $this->container->setIndent(5); + $this->assertEquals(' ', $this->container->getIndent()); + } + + /** + * @return void + */ + public function testSetIndentImplementsFluentInterface() + { + $result = $this->container->setIndent(' '); + $this->assertSame($this->container, $result); + } + + /** + * @return void + */ + public function testCapturingToPlaceholderStoresContent() + { + $this->container->captureStart(); + echo 'This is content intended for capture'; + $this->container->captureEnd(); + + $value = $this->container->getValue(); + $this->assertContains('This is content intended for capture', $value); + } + + /** + * @return void + */ + public function testCapturingToPlaceholderAppendsContent() + { + $this->container[] = 'foo'; + $originalCount = count($this->container); + + $this->container->captureStart(); + echo 'This is content intended for capture'; + $this->container->captureEnd(); + + $this->assertEquals($originalCount + 1, count($this->container)); + + $value = $this->container->getValue(); + $keys = array_keys($value); + $lastIndex = array_pop($keys); + $this->assertEquals('foo', $value[$lastIndex - 1]); + $this->assertContains('This is content intended for capture', $value[$lastIndex]); + } + + /** + * @return void + */ + public function testCapturingToPlaceholderUsingPrependPrependsContent() + { + $this->container[] = 'foo'; + $originalCount = count($this->container); + + $this->container->captureStart('PREPEND'); + echo 'This is content intended for capture'; + $this->container->captureEnd(); + + $this->assertEquals($originalCount + 1, count($this->container)); + + $value = $this->container->getValue(); + $keys = array_keys($value); + $lastIndex = array_pop($keys); + $this->assertEquals('foo', $value[$lastIndex]); + $this->assertContains('This is content intended for capture', $value[$lastIndex - 1]); + } + + /** + * @return void + */ + public function testCapturingToPlaceholderUsingSetOverwritesContent() + { + $this->container[] = 'foo'; + $this->container->captureStart('SET'); + echo 'This is content intended for capture'; + $this->container->captureEnd(); + + $this->assertEquals(1, count($this->container)); + + $value = $this->container->getValue(); + $this->assertContains('This is content intended for capture', $value); + } + + /** + * @return void + */ + public function testCapturingToPlaceholderKeyUsingSetCapturesContent() + { + $this->container->captureStart('SET', 'key'); + echo 'This is content intended for capture'; + $this->container->captureEnd(); + + $this->assertEquals(1, count($this->container)); + $this->assertTrue(isset($this->container['key'])); + $value = $this->container['key']; + $this->assertContains('This is content intended for capture', $value); + } + + /** + * @return void + */ + public function testCapturingToPlaceholderKeyUsingSetReplacesContentAtKey() + { + $this->container['key'] = 'Foobar'; + $this->container->captureStart('SET', 'key'); + echo 'This is content intended for capture'; + $this->container->captureEnd(); + + $this->assertEquals(1, count($this->container)); + $this->assertTrue(isset($this->container['key'])); + $value = $this->container['key']; + $this->assertContains('This is content intended for capture', $value); + } + + /** + * @return void + */ + public function testCapturingToPlaceholderKeyUsingAppendAppendsContentAtKey() + { + $this->container['key'] = 'Foobar '; + $this->container->captureStart('APPEND', 'key'); + echo 'This is content intended for capture'; + $this->container->captureEnd(); + + $this->assertEquals(1, count($this->container)); + $this->assertTrue(isset($this->container['key'])); + $value = $this->container['key']; + $this->assertContains('Foobar This is content intended for capture', $value); + } + + /** + * @return void + */ + public function testNestedCapturesThrowsException() + { + $this->container[] = 'foo'; + $caught = false; + try { + $this->container->captureStart('SET'); + $this->container->captureStart('SET'); + $this->container->captureEnd(); + $this->container->captureEnd(); + } catch (\Exception $e) { + $this->container->captureEnd(); + $caught = true; + } + + $this->assertTrue($caught, 'Nested captures should throw exceptions'); + } + + /** + * @return void + */ + public function testToStringWithNoModifiersAndSingleValueReturnsValue() + { + $this->container->set('foo'); + $value = $this->container->toString(); + $this->assertEquals($this->container->getValue(), $value); + } + + /** + * @return void + */ + public function testToStringWithModifiersAndSingleValueReturnsFormattedValue() + { + $this->container->set('foo'); + $this->container->setPrefix('
    • ') + ->setPostfix('
    • '); + $value = $this->container->toString(); + $this->assertEquals('
    • foo
    • ', $value); + } + + /** + * @return void + */ + public function testToStringWithNoModifiersAndCollectionReturnsImplodedString() + { + $this->container[] = 'foo'; + $this->container[] = 'bar'; + $this->container[] = 'baz'; + $value = $this->container->toString(); + $this->assertEquals('foobarbaz', $value); + } + + /** + * @return void + */ + public function testToStringWithModifiersAndCollectionReturnsFormattedString() + { + $this->container[] = 'foo'; + $this->container[] = 'bar'; + $this->container[] = 'baz'; + $this->container->setPrefix('
      • ') + ->setSeparator('
      • ') + ->setPostfix('
      '); + $value = $this->container->toString(); + $this->assertEquals('
      • foo
      • bar
      • baz
      ', $value); + } + + /** + * @return void + */ + public function testToStringWithModifiersAndCollectionReturnsFormattedStringWithIndentation() + { + $this->container[] = 'foo'; + $this->container[] = 'bar'; + $this->container[] = 'baz'; + $this->container->setPrefix('
      • ') + ->setSeparator('
      • ' . PHP_EOL . '
      • ') + ->setPostfix('
      ') + ->setIndent(' '); + $value = $this->container->toString(); + $expectedValue = '
      • foo
      • ' . PHP_EOL . '
      • bar
      • ' . PHP_EOL . '
      • baz
      '; + $this->assertEquals($expectedValue, $value); + } + + /** + * @return void + */ + public function test__toStringProxiesToToString() + { + $this->container[] = 'foo'; + $this->container[] = 'bar'; + $this->container[] = 'baz'; + $this->container->setPrefix('
      • ') + ->setSeparator('
      • ') + ->setPostfix('
      '); + $value = $this->container->__toString(); + $this->assertEquals('
      • foo
      • bar
      • baz
      ', $value); + } + + /** + * @return void + */ + public function testPrependPushesValueToTopOfContainer() + { + $this->container['foo'] = 'bar'; + $this->container->prepend('baz'); + + $expected = array('baz', 'foo' => 'bar'); + $array = $this->container->getArrayCopy(); + $this->assertSame($expected, $array); + } + + public function testIndentationIsHonored() + { + $this->container->setIndent(4) + ->setPrefix("
        \n
      • ") + ->setSeparator("
      • \n
      • ") + ->setPostfix("
      • \n
      "); + $this->container->append('foo'); + $this->container->append('bar'); + $this->container->append('baz'); + $string = $this->container->toString(); + + $lis = substr_count($string, "\n
    • "); + $this->assertEquals(3, $lis); + $this->assertTrue((strstr($string, "
        \n")) ? true : false, $string); + $this->assertTrue((strstr($string, "\n
      ")) ? true : false); + } +} + diff --git a/test/Helper/Placeholder/RegistryTest.php b/test/Helper/Placeholder/RegistryTest.php new file mode 100644 index 00000000..312e3873 --- /dev/null +++ b/test/Helper/Placeholder/RegistryTest.php @@ -0,0 +1,185 @@ +registry = new Registry(); + } + + /** + * Tears down the fixture, for example, close a network connection. + * This method is called after a test is executed. + * + * @return void + */ + public function tearDown() + { + unset($this->registry); + } + + /** + * @return void + */ + public function testCreateContainer() + { + $this->assertFalse($this->registry->containerExists('foo')); + $this->registry->createContainer('foo'); + $this->assertTrue($this->registry->containerExists('foo')); + } + + /** + * @return void + */ + public function testCreateContainerCreatesDefaultContainerClass() + { + $this->assertFalse($this->registry->containerExists('foo')); + $container = $this->registry->createContainer('foo'); + $this->assertTrue($container instanceof Container); + } + + /** + * @return void + */ + public function testGetContainerCreatesContainerIfNonExistent() + { + $this->assertFalse($this->registry->containerExists('foo')); + $container = $this->registry->getContainer('foo'); + $this->assertTrue($container instanceof Container\AbstractContainer); + $this->assertTrue($this->registry->containerExists('foo')); + } + + /** + * @return void + */ + public function testSetContainerCreatesRegistryEntry() + { + $foo = new Container(array('foo', 'bar')); + $this->assertFalse($this->registry->containerExists('foo')); + $this->registry->setContainer('foo', $foo); + $this->assertTrue($this->registry->containerExists('foo')); + } + + public function testSetContainerCreatesRegistersContainerInstance() + { + $foo = new Container(array('foo', 'bar')); + $this->assertFalse($this->registry->containerExists('foo')); + $this->registry->setContainer('foo', $foo); + $container = $this->registry->getContainer('foo'); + $this->assertSame($foo, $container); + } + + public function testContainerClassAccessorsSetState() + { + $this->assertEquals('Zend\View\Helper\Placeholder\Container', $this->registry->getContainerClass()); + $this->registry->setContainerClass('ZendTest\View\Helper\Placeholder\MockContainer'); + $this->assertEquals('ZendTest\View\Helper\Placeholder\MockContainer', $this->registry->getContainerClass()); + } + + public function testSetContainerClassThrowsExceptionWithInvalidContainerClass() + { + try { + $this->registry->setContainerClass('ZendTest\View\Helper\Placeholder\BogusContainer'); + $this->fail('Invalid container classes should not be accepted'); + } catch (\Exception $e) { + } + } + + public function testDeletingContainerRemovesFromRegistry() + { + $this->registry->createContainer('foo'); + $this->assertTrue($this->registry->containerExists('foo')); + $result = $this->registry->deleteContainer('foo'); + $this->assertFalse($this->registry->containerExists('foo')); + $this->assertTrue($result); + } + + public function testDeleteContainerReturnsFalseIfContainerDoesNotExist() + { + $result = $this->registry->deleteContainer('foo'); + $this->assertFalse($result); + } + + public function testUsingCustomContainerClassCreatesContainersOfCustomClass() + { + $this->registry->setContainerClass('ZendTest\View\Helper\Placeholder\MockContainer'); + $container = $this->registry->createContainer('foo'); + $this->assertTrue($container instanceof MockContainer); + } + + public function testGetRegistryReturnsRegistryInstance() + { + $registry = Registry::getRegistry(); + $this->assertTrue($registry instanceof Registry); + } + + public function testGetRegistrySubsequentTimesReturnsSameInstance() + { + $registry1 = Registry::getRegistry(); + $registry2 = Registry::getRegistry(); + $this->assertSame($registry1, $registry2); + } + + /** + * @group ZF-10793 + */ + public function testSetValueCreateContainer() + { + $this->registry->setContainerClass('ZendTest\View\Helper\Placeholder\MockContainer'); + $data = array( + 'ZF-10793' + ); + $container = $this->registry->createContainer('foo', $data); + $this->assertEquals(array('ZF-10793'), $container->data); + } +} + +class MockContainer extends Container\AbstractContainer +{ + public $data = array(); + + public function __construct($data) + { + $this->data = $data; + } +} + +class BogusContainer +{ +} diff --git a/test/Helper/Placeholder/StandaloneContainerTest.php b/test/Helper/Placeholder/StandaloneContainerTest.php new file mode 100644 index 00000000..6f4e5119 --- /dev/null +++ b/test/Helper/Placeholder/StandaloneContainerTest.php @@ -0,0 +1,78 @@ +basePath = __DIR__ . '/_files/modules'; + $this->helper = new Foo(); + } + + /** + * Tears down the fixture, for example, close a network connection. + * This method is called after a test is executed. + * + * @return void + */ + public function tearDown() + { + unset($this->helper); + } + + public function testViewAccessorWorks() + { + $view = new View(); + $this->helper->setView($view); + $this->assertSame($view, $this->helper->getView()); + } + + public function testContainersPersistBetweenInstances() + { + $foo1 = new Foo; + $foo1->append('Foo'); + $foo1->setSeparator(' - '); + + $foo2 = new Foo; + $foo2->append('Bar'); + + $test = $foo1->toString(); + $this->assertContains('Foo', $test); + $this->assertContains(' - ', $test); + $this->assertContains('Bar', $test); + } +} + +class Foo extends \Zend\View\Helper\Placeholder\Container\AbstractStandalone +{ + protected $_regKey = 'foo'; + public function direct() {} +} diff --git a/test/Helper/PlaceholderTest.php b/test/Helper/PlaceholderTest.php new file mode 100644 index 00000000..035f4608 --- /dev/null +++ b/test/Helper/PlaceholderTest.php @@ -0,0 +1,91 @@ +placeholder = new Helper\Placeholder(); + } + + /** + * Tears down the fixture, for example, close a network connection. + * This method is called after a test is executed. + * + * @return void + */ + public function tearDown() + { + unset($this->placeholder); + PlaceholderRegistry::unsetRegistry(); + } + + public function testMultiplePlaceholdersUseSameRegistry() + { + $placeholder = new Helper\Placeholder(); + $this->assertSame($this->placeholder->getRegistry(), $placeholder->getRegistry()); + } + + /** + * @return void + */ + public function testSetView() + { + $view = new View(); + $this->placeholder->setView($view); + $this->assertSame($view, $this->placeholder->getView()); + } + + /** + * @return void + */ + public function testPlaceholderRetrievesContainer() + { + $container = $this->placeholder->__invoke('foo'); + $this->assertInstanceOf('Zend\View\Helper\Placeholder\Container\AbstractContainer', $container); + } + + /** + * @return void + */ + public function testPlaceholderRetrievesSameContainerOnSubsequentCalls() + { + $container1 = $this->placeholder->__invoke('foo'); + $container2 = $this->placeholder->__invoke('foo'); + $this->assertSame($container1, $container2); + } +} diff --git a/test/Helper/RenderChildModelTest.php b/test/Helper/RenderChildModelTest.php new file mode 100644 index 00000000..97d2afdd --- /dev/null +++ b/test/Helper/RenderChildModelTest.php @@ -0,0 +1,133 @@ +resolver = new TemplateMapResolver(array( + 'layout' => __DIR__ . '/../_templates/nested-view-model-layout.phtml', + 'child1' => __DIR__ . '/../_templates/nested-view-model-content.phtml', + 'child2' => __DIR__ . '/../_templates/nested-view-model-child2.phtml', + 'complex' => __DIR__ . '/../_templates/nested-view-model-complexlayout.phtml', + )); + $this->renderer = $renderer = new PhpRenderer(); + $renderer->setCanRenderTrees(true); + $renderer->setResolver($this->resolver); + + $this->viewModelHelper = $renderer->plugin('view_model'); + $this->helper = $renderer->plugin('render_child_model'); + + $this->parent = new ViewModel(); + $this->parent->setTemplate('layout'); + $this->viewModelHelper->setRoot($this->parent); + $this->viewModelHelper->setCurrent($this->parent); + } + + public function testRendersEmptyStringWhenUnableToResolveChildModel() + { + $result = $this->helper->render('child1'); + $this->assertSame('', $result); + } + + public function setupFirstChild() + { + $child1 = new ViewModel(); + $child1->setTemplate('child1'); + $child1->setCaptureTo('child1'); + $this->parent->addChild($child1); + return $child1; + } + + public function testRendersChildTemplateWhenAbleToResolveChildModelByCaptureToValue() + { + $this->setupFirstChild(); + $result = $this->helper->render('child1'); + $this->assertContains('Content for layout', $result, $result); + } + + public function setupSecondChild() + { + $child2 = new ViewModel(); + $child2->setTemplate('child2'); + $child2->setCaptureTo('child2'); + $this->parent->addChild($child2); + return $child2; + } + + + public function testRendersSiblingChildrenWhenCalledInSequence() + { + $this->setupFirstChild(); + $this->setupSecondChild(); + $result = $this->helper->render('child1'); + $this->assertContains('Content for layout', $result, $result); + $result = $this->helper->render('child2'); + $this->assertContains('Second child', $result, $result); + } + + public function testRendersNestedChildren() + { + $child1 = $this->setupFirstChild(); + $child1->setTemplate('layout'); + $child2 = new ViewModel(); + $child2->setTemplate('child1'); + $child2->setCaptureTo('content'); + $child1->addChild($child2); + + $result = $this->helper->render('child1'); + $this->assertContains('Layout start', $result, $result); + $this->assertContains('Content for layout', $result, $result); + $this->assertContains('Layout end', $result, $result); + } + + public function testRendersSequentialChildrenWithNestedChildren() + { + $this->parent->setTemplate('complex'); + $child1 = $this->setupFirstChild(); + $child1->setTemplate('layout'); + $child1->setCaptureTo('content'); + + $child2 = $this->setupSecondChild(); + $child2->setCaptureTo('sidebar'); + + $nested = new ViewModel(); + $nested->setTemplate('child1'); + $nested->setCaptureTo('content'); + $child1->addChild($nested); + + $result = $this->renderer->render($this->parent); + $this->assertRegExp('/Content:\s+Layout start\s+Content for layout\s+Layout end\s+Sidebar:\s+Second child/s', $result, $result); + } + + public function testAttemptingToRenderWithNoCurrentModelRaisesException() + { + $renderer = new PhpRenderer(); + $renderer->setResolver($this->resolver); + $this->setExpectedException('Zend\View\Exception\RuntimeException', 'no view model'); + $this->expectOutputString("Layout start" . PHP_EOL . PHP_EOL); + $renderer->render('layout'); + } +} diff --git a/test/Helper/RenderToPlaceholderTest.php b/test/Helper/RenderToPlaceholderTest.php new file mode 100644 index 00000000..00aaf712 --- /dev/null +++ b/test/Helper/RenderToPlaceholderTest.php @@ -0,0 +1,42 @@ +_view = new View(); + $this->_view->resolver()->addPath(__DIR__.'/_files/scripts/'); + } + + public function testDefaultEmpty() + { + $this->_view->plugin('renderToPlaceholder')->__invoke('rendertoplaceholderscript.phtml', 'fooPlaceholder'); + $placeholder = new PlaceholderHelper(); + $this->assertEquals("Foo Bar" . "\n", $placeholder->__invoke('fooPlaceholder')->getValue()); + } + +} + diff --git a/test/Helper/ServerUrlTest.php b/test/Helper/ServerUrlTest.php new file mode 100644 index 00000000..13a1d535 --- /dev/null +++ b/test/Helper/ServerUrlTest.php @@ -0,0 +1,186 @@ +_serverBackup = $_SERVER; + unset($_SERVER['HTTPS']); + } + + /** + * Cleans up the environment after running a test. + */ + protected function tearDown() + { + $_SERVER = $this->_serverBackup; + } + + public function testConstructorWithOnlyHost() + { + $_SERVER['HTTP_HOST'] = 'example.com'; + + $url = new Helper\ServerUrl(); + $this->assertEquals('http://example.com', $url->__invoke()); + } + + public function testConstructorWithOnlyHostIncludingPort() + { + $_SERVER['HTTP_HOST'] = 'example.com:8000'; + + $url = new Helper\ServerUrl(); + $this->assertEquals('http://example.com:8000', $url->__invoke()); + } + + public function testConstructorWithHostAndHttpsOn() + { + $_SERVER['HTTP_HOST'] = 'example.com'; + $_SERVER['HTTPS'] = 'on'; + + $url = new Helper\ServerUrl(); + $this->assertEquals('https://example.com', $url->__invoke()); + } + + public function testConstructorWithHostAndHttpsTrue() + { + $_SERVER['HTTP_HOST'] = 'example.com'; + $_SERVER['HTTPS'] = true; + + $url = new Helper\ServerUrl(); + $this->assertEquals('https://example.com', $url->__invoke()); + } + + public function testConstructorWithHostIncludingPortAndHttpsTrue() + { + $_SERVER['HTTP_HOST'] = 'example.com:8181'; + $_SERVER['HTTPS'] = true; + + $url = new Helper\ServerUrl(); + $this->assertEquals('https://example.com:8181', $url->__invoke()); + } + + public function testConstructorWithHttpHostAndServerNameAndPortSet() + { + $_SERVER['HTTP_HOST'] = 'example.com'; + $_SERVER['SERVER_NAME'] = 'example.org'; + $_SERVER['SERVER_PORT'] = 8080; + + $url = new Helper\ServerUrl(); + $this->assertEquals('http://example.com', $url->__invoke()); + } + + public function testConstructorWithNoHttpHostButServerNameAndPortSet() + { + unset($_SERVER['HTTP_HOST']); + $_SERVER['SERVER_NAME'] = 'example.org'; + $_SERVER['SERVER_PORT'] = 8080; + + $url = new Helper\ServerUrl(); + $this->assertEquals('http://example.org:8080', $url->__invoke()); + } + + public function testServerUrlWithTrueParam() + { + $_SERVER['HTTPS'] = 'off'; + $_SERVER['HTTP_HOST'] = 'example.com'; + $_SERVER['REQUEST_URI'] = '/foo.html'; + + $url = new Helper\ServerUrl(); + $this->assertEquals('http://example.com/foo.html', $url->__invoke(true)); + } + + public function testServerUrlWithInteger() + { + $_SERVER['HTTPS'] = 'off'; + $_SERVER['HTTP_HOST'] = 'example.com'; + $_SERVER['REQUEST_URI'] = '/foo.html'; + + $url = new Helper\ServerUrl(); + $this->assertEquals('http://example.com', $url->__invoke(1337)); + } + + public function testServerUrlWithObject() + { + $_SERVER['HTTPS'] = 'off'; + $_SERVER['HTTP_HOST'] = 'example.com'; + $_SERVER['REQUEST_URI'] = '/foo.html'; + + $url = new Helper\ServerUrl(); + $this->assertEquals('http://example.com', $url->__invoke(new \stdClass())); + } + + /** + * @group ZF-9919 + */ + public function testServerUrlWithScheme() + { + $_SERVER['HTTP_SCHEME'] = 'https'; + $_SERVER['HTTP_HOST'] = 'example.com'; + $url = new Helper\ServerUrl(); + $this->assertEquals('https://example.com', $url->__invoke()); + } + + /** + * @group ZF-9919 + */ + public function testServerUrlWithPort() + { + $_SERVER['SERVER_PORT'] = 443; + $_SERVER['HTTP_HOST'] = 'example.com'; + $url = new Helper\ServerUrl(); + $this->assertEquals('https://example.com', $url->__invoke()); + } + + /** + * @group ZF2-508 + */ + public function testServerUrlWithProxy() + { + $_SERVER['HTTP_HOST'] = 'proxyserver.com'; + $_SERVER['HTTP_X_FORWARDED_HOST'] = 'www.firsthost.org'; + $url = new Helper\ServerUrl(); + $this->assertEquals('http://www.firsthost.org', $url->__invoke()); + } + + /** + * @group ZF2-508 + */ + public function testServerUrlWithMultipleProxies() + { + $_SERVER['HTTP_HOST'] = 'proxyserver.com'; + $_SERVER['HTTP_X_FORWARDED_HOST'] = 'www.firsthost.org, www.secondhost.org'; + $url = new Helper\ServerUrl(); + $this->assertEquals('http://www.secondhost.org', $url->__invoke()); + } +} diff --git a/test/Helper/TestAsset/ArrayTranslator.php b/test/Helper/TestAsset/ArrayTranslator.php new file mode 100644 index 00000000..a2528db7 --- /dev/null +++ b/test/Helper/TestAsset/ArrayTranslator.php @@ -0,0 +1,24 @@ +translations); + return $textDomain; + } +} diff --git a/test/Helper/TestAsset/ConcreteHelper.php b/test/Helper/TestAsset/ConcreteHelper.php new file mode 100644 index 00000000..2803224d --- /dev/null +++ b/test/Helper/TestAsset/ConcreteHelper.php @@ -0,0 +1,21 @@ +array; + } +} diff --git a/test/Helper/UrlTest.php b/test/Helper/UrlTest.php new file mode 100644 index 00000000..6bc6e4af --- /dev/null +++ b/test/Helper/UrlTest.php @@ -0,0 +1,135 @@ +addRoute('home', array( + 'type' => 'Zend\Mvc\Router\Http\Literal', + 'options' => array( + 'route' => '/', + ) + )); + $router->addRoute('default', array( + 'type' => 'Zend\Mvc\Router\Http\Segment', + 'options' => array( + 'route' => '/:controller[/:action]', + ) + )); + $this->router = $router; + + $this->url = new UrlHelper; + $this->url->setRouter($router); + } + + public function testHelperHasHardDependencyWithRouter() + { + $this->setExpectedException('Zend\View\Exception\RuntimeException', 'No RouteStackInterface instance provided'); + $url = new UrlHelper; + $url('home'); + } + + public function testHomeRoute() + { + $url = $this->url->__invoke('home'); + $this->assertEquals('/', $url); + } + + public function testModuleRoute() + { + $url = $this->url->__invoke('default', array('controller' => 'ctrl', 'action' => 'act')); + $this->assertEquals('/ctrl/act', $url); + } + + public function testPluginWithoutRouteMatchesInEventRaisesExceptionWhenNoRouteProvided() + { + $this->setExpectedException('Zend\View\Exception\RuntimeException', 'RouteMatch'); + $url = $this->url->__invoke(); + } + + public function testPluginWithRouteMatchesReturningNoMatchedRouteNameRaisesExceptionWhenNoRouteProvided() + { + $this->url->setRouteMatch(new RouteMatch(array())); + $this->setExpectedException('Zend\View\Exception\RuntimeException', 'matched'); + $url = $this->url->__invoke(); + } + + public function testPassingNoArgumentsWithValidRouteMatchGeneratesUrl() + { + $routeMatch = new RouteMatch(array()); + $routeMatch->setMatchedRouteName('home'); + $this->url->setRouteMatch($routeMatch); + $url = $this->url->__invoke(); + $this->assertEquals('/', $url); + } + + public function testCanReuseMatchedParameters() + { + $this->router->addRoute('replace', array( + 'type' => 'Zend\Mvc\Router\Http\Segment', + 'options' => array( + 'route' => '/:controller/:action', + 'defaults' => array( + 'controller' => 'ZendTest\Mvc\Controller\TestAsset\SampleController', + ), + ), + )); + $routeMatch = new RouteMatch(array( + 'controller' => 'foo', + )); + $routeMatch->setMatchedRouteName('replace'); + $this->url->setRouteMatch($routeMatch); + $url = $this->url->__invoke('replace', array('action' => 'bar'), array(), true); + $this->assertEquals('/foo/bar', $url); + } + + public function testCanPassBooleanValueForThirdArgumentToAllowReusingRouteMatches() + { + $this->router->addRoute('replace', array( + 'type' => 'Zend\Mvc\Router\Http\Segment', + 'options' => array( + 'route' => '/:controller/:action', + 'defaults' => array( + 'controller' => 'ZendTest\Mvc\Controller\TestAsset\SampleController', + ), + ), + )); + $routeMatch = new RouteMatch(array( + 'controller' => 'foo', + )); + $routeMatch->setMatchedRouteName('replace'); + $this->url->setRouteMatch($routeMatch); + $url = $this->url->__invoke('replace', array('action' => 'bar'), true); + $this->assertEquals('/foo/bar', $url); + } +} diff --git a/test/Helper/_files/modules/application/views/scripts/action-bar/baz.phtml b/test/Helper/_files/modules/application/views/scripts/action-bar/baz.phtml new file mode 100644 index 00000000..86f125f4 --- /dev/null +++ b/test/Helper/_files/modules/application/views/scripts/action-bar/baz.phtml @@ -0,0 +1,5 @@ + + +Hello \ No newline at end of file diff --git a/test/Helper/_files/modules/application/views/scripts/action-foo/bar.phtml b/test/Helper/_files/modules/application/views/scripts/action-foo/bar.phtml new file mode 100644 index 00000000..c22e27e6 --- /dev/null +++ b/test/Helper/_files/modules/application/views/scripts/action-foo/bar.phtml @@ -0,0 +1 @@ +In default module, FooController::barAction() diff --git a/test/Helper/_files/modules/application/views/scripts/action-foo/baz.phtml b/test/Helper/_files/modules/application/views/scripts/action-foo/baz.phtml new file mode 100644 index 00000000..c209133d --- /dev/null +++ b/test/Helper/_files/modules/application/views/scripts/action-foo/baz.phtml @@ -0,0 +1 @@ +Message: message ?> diff --git a/test/Helper/_files/modules/application/views/scripts/partialActionCall.phtml b/test/Helper/_files/modules/application/views/scripts/partialActionCall.phtml new file mode 100644 index 00000000..3a1a4a57 --- /dev/null +++ b/test/Helper/_files/modules/application/views/scripts/partialActionCall.phtml @@ -0,0 +1,2 @@ +action('baz', 'action-bar', 'default', array('hello' => 'goodby')); diff --git a/test/Helper/_files/modules/application/views/scripts/partialLoop.phtml b/test/Helper/_files/modules/application/views/scripts/partialLoop.phtml new file mode 100644 index 00000000..add9b18f --- /dev/null +++ b/test/Helper/_files/modules/application/views/scripts/partialLoop.phtml @@ -0,0 +1,2 @@ +This is an iteration: vars()->message ?> + diff --git a/test/Helper/_files/modules/application/views/scripts/partialLoopCouter.phtml b/test/Helper/_files/modules/application/views/scripts/partialLoopCouter.phtml new file mode 100644 index 00000000..02097982 --- /dev/null +++ b/test/Helper/_files/modules/application/views/scripts/partialLoopCouter.phtml @@ -0,0 +1,2 @@ +This is an iteration: vars()->message ?>, pointer at partialCounter ?> + diff --git a/test/Helper/_files/modules/application/views/scripts/partialLoopObject.phtml b/test/Helper/_files/modules/application/views/scripts/partialLoopObject.phtml new file mode 100644 index 00000000..aa5005a3 --- /dev/null +++ b/test/Helper/_files/modules/application/views/scripts/partialLoopObject.phtml @@ -0,0 +1,6 @@ +vars()->obj)): ?> +No object model passed +vars()->obj->message . "\n"; +endif ?> + diff --git a/test/Helper/_files/modules/application/views/scripts/partialObj.phtml b/test/Helper/_files/modules/application/views/scripts/partialObj.phtml new file mode 100644 index 00000000..bd13b563 --- /dev/null +++ b/test/Helper/_files/modules/application/views/scripts/partialObj.phtml @@ -0,0 +1,11 @@ +vars()->foo)): ?> +No object model passed +vars()->foo); + foreach ($vars as $key => $value): ?> +: + + + + diff --git a/test/Helper/_files/modules/application/views/scripts/partialOne.phtml b/test/Helper/_files/modules/application/views/scripts/partialOne.phtml new file mode 100644 index 00000000..83c5a413 --- /dev/null +++ b/test/Helper/_files/modules/application/views/scripts/partialOne.phtml @@ -0,0 +1 @@ +This is the first test partial diff --git a/test/Helper/_files/modules/application/views/scripts/partialThree.phtml b/test/Helper/_files/modules/application/views/scripts/partialThree.phtml new file mode 100644 index 00000000..816e6522 --- /dev/null +++ b/test/Helper/_files/modules/application/views/scripts/partialThree.phtml @@ -0,0 +1,2 @@ +Partial with variables: +vars()->message ?> diff --git a/test/Helper/_files/modules/application/views/scripts/partialVars.phtml b/test/Helper/_files/modules/application/views/scripts/partialVars.phtml new file mode 100644 index 00000000..0124bc0a --- /dev/null +++ b/test/Helper/_files/modules/application/views/scripts/partialVars.phtml @@ -0,0 +1,4 @@ +vars() as $key => $value) { + printf("%s: %s\n", $key, $value); +} diff --git a/test/Helper/_files/scripts/rendertoplaceholderscript.phtml b/test/Helper/_files/scripts/rendertoplaceholderscript.phtml new file mode 100644 index 00000000..76c7ac2d --- /dev/null +++ b/test/Helper/_files/scripts/rendertoplaceholderscript.phtml @@ -0,0 +1 @@ +Foo Bar diff --git a/test/Helper/_files/scripts/testPagination.phtml b/test/Helper/_files/scripts/testPagination.phtml new file mode 100644 index 00000000..efbf90f7 --- /dev/null +++ b/test/Helper/_files/scripts/testPagination.phtml @@ -0,0 +1,3 @@ +pagination control + +page count (vars('pageCount'); ?>) vars('pageCount') == count($this->vars('pagesInRange'))) ? 'equals' : 'does not equal'; ?> pages in range (vars('pagesInRange')); ?>) diff --git a/test/HelperPluginManagerTest.php b/test/HelperPluginManagerTest.php new file mode 100644 index 00000000..fd5c8142 --- /dev/null +++ b/test/HelperPluginManagerTest.php @@ -0,0 +1,67 @@ +helpers = new HelperPluginManager(); + } + + public function testViewIsNullByDefault() + { + $this->assertNull($this->helpers->getRenderer()); + } + + public function testAllowsInjectingRenderer() + { + $renderer = new PhpRenderer(); + $this->helpers->setRenderer($renderer); + $this->assertSame($renderer, $this->helpers->getRenderer()); + } + + public function testInjectsRendererToHelperWhenRendererIsPresent() + { + $renderer = new PhpRenderer(); + $this->helpers->setRenderer($renderer); + $helper = $this->helpers->get('doctype'); + $this->assertSame($renderer, $helper->getView()); + } + + public function testNoRendererInjectedInHelperWhenRendererIsNotPresent() + { + $helper = $this->helpers->get('doctype'); + $this->assertNull($helper->getView()); + } + + public function testRegisteringInvalidHelperRaisesException() + { + $this->setExpectedException('Zend\View\Exception\InvalidHelperException'); + $this->helpers->setService('test', $this); + } + + public function testLoadingInvalidHelperRaisesException() + { + $this->helpers->setInvokableClass('test', get_class($this)); + $this->setExpectedException('Zend\View\Exception\InvalidHelperException'); + $this->helpers->get('test'); + } +} diff --git a/test/Model/JsonModelTest.php b/test/Model/JsonModelTest.php new file mode 100644 index 00000000..45686d41 --- /dev/null +++ b/test/Model/JsonModelTest.php @@ -0,0 +1,50 @@ +assertInstanceOf('Zend\View\Variables', $model->getVariables()); + $this->assertEquals(array(), $model->getOptions()); + } + + public function testCanSerializeVariablesToJson() + { + $array = array('foo' => 'bar'); + $model = new JsonModel($array); + $this->assertEquals($array, $model->getVariables()); + $this->assertEquals(Json::encode($array), $model->serialize()); + } + + + public function testCanSerializeWithJsonpCallback() + { + $array = array('foo' => 'bar'); + $model = new JsonModel($array); + $model->setJsonpCallback('callback'); + $this->assertEquals('callback(' . Json::encode($array) . ');', $model->serialize()); + } +} diff --git a/test/Model/TestAsset/Variable.php b/test/Model/TestAsset/Variable.php new file mode 100644 index 00000000..f1569105 --- /dev/null +++ b/test/Model/TestAsset/Variable.php @@ -0,0 +1,36 @@ +assertInstanceOf('Zend\View\Variables', $model->getVariables()); + $this->assertEquals(array(), $model->getOptions()); + } + + public function testAllowsEmptyOptionsArgumentToConstructor() + { + $model = new ViewModel(array('foo' => 'bar')); + $this->assertEquals(array('foo' => 'bar'), $model->getVariables()); + $this->assertEquals(array(), $model->getOptions()); + } + + public function testAllowsPassingBothVariablesAndOptionsArgumentsToConstructor() + { + $model = new ViewModel(array('foo' => 'bar'), array('template' => 'foo/bar')); + $this->assertEquals(array('foo' => 'bar'), $model->getVariables()); + $this->assertEquals(array('template' => 'foo/bar'), $model->getOptions()); + } + + public function testAllowsPassingTraversableArgumentsToVariablesAndOptionsInConstructor() + { + $vars = new ArrayObject; + $options = new ArrayObject; + $model = new ViewModel($vars, $options); + $this->assertSame($vars, $model->getVariables()); + $this->assertSame(iterator_to_array($options), $model->getOptions()); + } + + public function testAllowsPassingNonArrayAccessObjectsAsArrayInConstructor() + { + $vars = array('foo' => new Variable); + $model = new ViewModel($vars); + $this->assertSame($vars, $model->getVariables()); + } + + public function testCanSetVariablesSingly() + { + $model = new ViewModel(array('foo' => 'bar')); + $model->setVariable('bar', 'baz'); + $this->assertEquals(array('foo' => 'bar', 'bar' => 'baz'), $model->getVariables()); + } + + public function testCanOverwriteVariablesSingly() + { + $model = new ViewModel(array('foo' => 'bar')); + $model->setVariable('foo', 'baz'); + $this->assertEquals(array('foo' => 'baz'), $model->getVariables()); + } + + public function testSetVariablesMergesWithPreviouslyStoredVariables() + { + $model = new ViewModel(array('foo' => 'bar', 'bar' => 'baz')); + $model->setVariables(array('bar' => 'BAZBAT')); + $this->assertEquals(array('foo' => 'bar', 'bar' => 'BAZBAT'), $model->getVariables()); + } + + public function testCanUnsetVariable() + { + $model = new ViewModel(array('foo' => 'bar')); + $model->__unset('foo'); + $this->assertEquals(array(), $model->getVariables()); + } + + public function testCanSetOptionsSingly() + { + $model = new ViewModel(array(), array('foo' => 'bar')); + $model->setOption('bar', 'baz'); + $this->assertEquals(array('foo' => 'bar', 'bar' => 'baz'), $model->getOptions()); + } + + public function testCanOverwriteOptionsSingly() + { + $model = new ViewModel(array(), array('foo' => 'bar')); + $model->setOption('foo', 'baz'); + $this->assertEquals(array('foo' => 'baz'), $model->getOptions()); + } + + public function testSetOptionsOverwritesAllPreviouslyStored() + { + $model = new ViewModel(array(), array('foo' => 'bar', 'bar' => 'baz')); + $model->setOptions(array('bar' => 'BAZBAT')); + $this->assertEquals(array('bar' => 'BAZBAT'), $model->getOptions()); + } + + public function testOptionsAreInternallyConvertedToAnArrayFromTraversables() + { + $options = new ArrayObject(array('foo' => 'bar')); + $model = new ViewModel(); + $model->setOptions($options); + $this->assertEquals($options->getArrayCopy(), $model->getOptions()); + } + + public function testPassingAnInvalidArgumentToSetVariablesRaisesAnException() + { + $model = new ViewModel(); + $this->setExpectedException('Zend\View\Exception\InvalidArgumentException', 'expects an array'); + $model->setVariables(new stdClass); + } + + public function testPassingAnInvalidArgumentToSetOptionsRaisesAnException() + { + $model = new ViewModel(); + $this->setExpectedException('Zend\View\Exception\InvalidArgumentException', 'expects an array'); + $model->setOptions(new stdClass); + } + + public function testCaptureToDefaultsToContent() + { + $model = new ViewModel(); + $this->assertEquals('content', $model->captureTo()); + } + + public function testCaptureToValueIsMutable() + { + $model = new ViewModel(); + $model->setCaptureTo('foo'); + $this->assertEquals('foo', $model->captureTo()); + } + + public function testHasNoChildrenByDefault() + { + $model = new ViewModel(); + $this->assertFalse($model->hasChildren()); + } + + public function testWhenNoChildrenCountIsZero() + { + $model = new ViewModel(); + $this->assertEquals(0, count($model)); + } + + public function testCanAddChildren() + { + $model = new ViewModel(); + $child = new ViewModel(); + $model->addChild($child); + $this->assertTrue($model->hasChildren()); + } + + public function testCanCountChildren() + { + $model = new ViewModel(); + $child = new ViewModel(); + $model->addChild($child); + $this->assertEquals(1, count($model)); + $model->addChild($child); + $this->assertEquals(2, count($model)); + } + + public function testCanIterateChildren() + { + $model = new ViewModel(); + $child = new ViewModel(); + $model->addChild($child); + $model->addChild($child); + $model->addChild($child); + + $count = 0; + foreach ($model as $childModel) { + $this->assertSame($child, $childModel); + $count++; + } + $this->assertEquals(3, $count); + } + + public function testTemplateIsEmptyByDefault() + { + $model = new ViewModel(); + $template = $model->getTemplate(); + $this->assertTrue(empty($template)); + } + + public function testTemplateIsMutable() + { + $model = new ViewModel(); + $model->setTemplate('foo'); + $this->assertEquals('foo', $model->getTemplate()); + } + + public function testIsNotTerminatedByDefault() + { + $model = new ViewModel(); + $this->assertFalse($model->terminate()); + } + + public function testTerminationFlagIsMutable() + { + $model = new ViewModel(); + $model->setTerminal(true); + $this->assertTrue($model->terminate()); + } + + public function testAddChildAllowsSpecifyingCaptureToValue() + { + $model = new ViewModel(); + $child = new ViewModel(); + $model->addChild($child, 'foo'); + $this->assertTrue($model->hasChildren()); + $this->assertEquals('foo', $child->captureTo()); + } + + public function testAllowsPassingViewVariablesContainerAsVariablesToConstructor() + { + $variables = new ViewVariables(); + $model = new ViewModel($variables); + $this->assertSame($variables, $model->getVariables()); + } + + public function testPassingOverwriteFlagWhenSettingVariablesOverwritesContainer() + { + $variables = new ViewVariables(array('foo' => 'bar')); + $model = new ViewModel($variables); + $overwrite = new ViewVariables(array('foo' => 'baz')); + $model->setVariables($overwrite, true); + $this->assertSame($overwrite, $model->getVariables()); + } + + public function testPropertyOverloadingGivesAccessToProperties() + { + $model = new ViewModel(); + $variables = $model->getVariables(); + $model->foo = 'bar'; + $this->assertTrue(isset($model->foo)); + $this->assertEquals('bar', $variables['foo']); + $this->assertEquals('bar', $model->foo); + + unset($model->foo); + $this->assertFalse(isset($model->foo)); + $this->assertFalse(isset($variables['foo'])); + } + + public function testPropertyOverloadingAllowsWritingPropertiesAfterSetVariablesHasBeenCalled() + { + $model = new ViewModel(); + $model->setVariables(array('foo' => 'bar')); + $model->bar = 'baz'; + + $this->assertTrue(isset($model->bar)); + $this->assertEquals('baz', $model->bar); + $variables = $model->getVariables(); + $this->assertTrue(isset($variables['bar'])); + $this->assertEquals('baz', $variables['bar']); + } +} diff --git a/test/PhpRendererTest.php b/test/PhpRendererTest.php new file mode 100644 index 00000000..2f5d977c --- /dev/null +++ b/test/PhpRendererTest.php @@ -0,0 +1,377 @@ +renderer = new PhpRenderer(); + } + + public function testEngineIsIdenticalToRenderer() + { + $this->assertSame($this->renderer, $this->renderer->getEngine()); + } + + public function testUsesTemplatePathStackAsDefaultResolver() + { + $this->assertInstanceOf('Zend\View\Resolver\TemplatePathStack', $this->renderer->resolver()); + } + + public function testCanSetResolverInstance() + { + $resolver = new TemplatePathStack(); + $this->renderer->setResolver($resolver); + $this->assertSame($resolver, $this->renderer->resolver()); + } + + public function testPassingNameToResolverReturnsScriptName() + { + $this->renderer->resolver()->addPath(__DIR__ . '/_templates'); + $filename = $this->renderer->resolver('test.phtml'); + $this->assertEquals(realpath(__DIR__ . '/_templates/test.phtml'), $filename); + } + + public function testUsesVariablesObjectForVarsByDefault() + { + $this->assertInstanceOf('Zend\View\Variables', $this->renderer->vars()); + } + + public function testCanSpecifyArrayAccessForVars() + { + $a = new \ArrayObject; + $this->renderer->setVars($a); + $this->assertSame($a->getArrayCopy(), $this->renderer->vars()->getArrayCopy()); + } + + public function testCanSpecifyArrayForVars() + { + $vars = array('foo' => 'bar'); + $this->renderer->setVars($vars); + $this->assertEquals($vars, $this->renderer->vars()->getArrayCopy()); + } + + public function testPassingArgumentToVarsReturnsValueFromThatKey() + { + $this->renderer->vars()->assign(array('foo' => 'bar')); + $this->assertEquals('bar', $this->renderer->vars('foo')); + } + + public function testUsesHelperPluginManagerByDefault() + { + $this->assertInstanceOf('Zend\View\HelperPluginManager', $this->renderer->getHelperPluginManager()); + } + + public function testPassingArgumentToPluginReturnsHelperByThatName() + { + $helper = $this->renderer->plugin('doctype'); + $this->assertInstanceOf('Zend\View\Helper\Doctype', $helper); + } + + public function testPassingStringOfUndefinedClassToSetHelperPluginManagerRaisesException() + { + $this->setExpectedException('Zend\View\Exception\ExceptionInterface', 'Invalid'); + $this->renderer->setHelperPluginManager('__foo__'); + } + + public function testPassingValidStringClassToSetHelperPluginManagerCreatesIt() + { + $this->renderer->setHelperPluginManager('Zend\View\HelperPluginManager'); + $this->assertInstanceOf('Zend\View\HelperPluginManager', $this->renderer->getHelperPluginManager()); + } + + public function invalidPluginManagers() + { + return array( + array(true), + array(1), + array(1.0), + array(array('foo')), + array(new \stdClass), + ); + } + + /** + * @dataProvider invalidPluginManagers + */ + public function testPassingInvalidArgumentToSetHelperPluginManagerRaisesException($plugins) + { + $this->setExpectedException('Zend\View\Exception\ExceptionInterface', 'must extend'); + $this->renderer->setHelperPluginManager($plugins); + } + + public function testInjectsSelfIntoHelperPluginManager() + { + $plugins = $this->renderer->getHelperPluginManager(); + $this->assertSame($this->renderer, $plugins->getRenderer()); + } + + public function testUsesFilterChainByDefault() + { + $this->assertInstanceOf('Zend\Filter\FilterChain', $this->renderer->getFilterChain()); + } + + public function testMaySetExplicitFilterChainInstance() + { + $filterChain = new FilterChain(); + $this->renderer->setFilterChain($filterChain); + $this->assertSame($filterChain, $this->renderer->getFilterChain()); + } + + public function testRenderingAllowsVariableSubstitutions() + { + $expected = 'foo INJECT baz'; + $this->renderer->vars()->assign(array('bar' => 'INJECT')); + $this->renderer->resolver()->addPath(__DIR__ . '/_templates'); + $test = $this->renderer->render('test.phtml'); + $this->assertContains($expected, $test); + } + + public function testRenderingFiltersContentWithFilterChain() + { + $expected = 'foo bar baz'; + $this->renderer->getFilterChain()->attach(function($content) { + return str_replace('INJECT', 'bar', $content); + }); + $this->renderer->vars()->assign(array('bar' => 'INJECT')); + $this->renderer->resolver()->addPath(__DIR__ . '/_templates'); + $test = $this->renderer->render('test.phtml'); + $this->assertContains($expected, $test); + } + + public function testCanAccessHelpersInTemplates() + { + $this->renderer->resolver()->addPath(__DIR__ . '/_templates'); + $content = $this->renderer->render('test-with-helpers.phtml'); + foreach (array('foo', 'bar', 'baz') as $value) { + $this->assertContains("
    • $value
    • ", $content); + } + } + + /** + * @group ZF2-68 + */ + public function testCanSpecifyArrayForVarsAndGetAlwaysArrayObject() + { + $vars = array('foo' => 'bar'); + $this->renderer->setVars($vars); + $this->assertTrue($this->renderer->vars() instanceof Variables); + } + + /** + * @group ZF2-68 + */ + public function testPassingVariablesObjectToSetVarsShouldUseItDirectory() + { + $vars = new Variables(array('foo' => '

      Bar

      ')); + $this->renderer->setVars($vars); + $this->assertSame($vars, $this->renderer->vars()); + } + + /** + * @group ZF2-86 + */ + public function testNestedRenderingRestoresVariablesCorrectly() + { + $expected = "inner\n

      content

      "; + $this->renderer->resolver()->addPath(__DIR__ . '/_templates'); + $test = $this->renderer->render('testNestedOuter.phtml', array('content' => '

      content

      ')); + $this->assertEquals($expected, $test); + } + + /** + * @group convenience-api + */ + public function testPropertyOverloadingShouldProxyToVariablesContainer() + { + $this->renderer->foo = '

      Bar

      '; + $this->assertEquals($this->renderer->vars('foo'), $this->renderer->foo); + } + + /** + * @group convenience-api + */ + public function testMethodOverloadingShouldReturnHelperInstanceIfNotInvokable() + { + $helpers = $this->renderer->getHelperPluginManager(); + $helpers->setInvokableClass('uninvokable', 'ZendTest\View\TestAsset\Uninvokable'); + $helper = $this->renderer->uninvokable(); + $this->assertInstanceOf('ZendTest\View\TestAsset\Uninvokable', $helper); + } + + /** + * @group convenience-api + */ + public function testMethodOverloadingShouldInvokeHelperIfInvokable() + { + $helpers = $this->renderer->getHelperPluginManager(); + $helpers->setInvokableClass('invokable', 'ZendTest\View\TestAsset\Invokable'); + $return = $this->renderer->invokable('it works!'); + $this->assertEquals('ZendTest\View\TestAsset\Invokable::__invoke: it works!', $return); + } + + /** + * @group convenience-api + */ + public function testGetMethodShouldRetrieveVariableFromVariableContainer() + { + $this->renderer->foo = '

      Bar

      '; + $foo = $this->renderer->get('foo'); + $this->assertSame($this->renderer->vars()->foo, $foo); + } + + /** + * @group convenience-api + */ + public function testRenderingLocalVariables() + { + $expected = '10 > 9'; + $this->renderer->vars()->assign(array('foo' => '10 > 9')); + $this->renderer->resolver()->addPath(__DIR__ . '/_templates'); + $test = $this->renderer->render('testLocalVars.phtml'); + $this->assertContains($expected, $test); + } + + public function testRendersTemplatesInAStack() + { + $resolver = new TemplateMapResolver(array( + 'layout' => __DIR__ . '/_templates/layout.phtml', + 'block' => __DIR__ . '/_templates/block.phtml', + )); + $this->renderer->setResolver($resolver); + + $content = $this->renderer->render('block'); + $this->assertRegexp('#\s*Block content\s*#', $content); + } + + /** + * @group view-model + */ + public function testCanRenderViewModel() + { + $resolver = new TemplateMapResolver(array( + 'empty' => __DIR__ . '/_templates/empty.phtml', + )); + $this->renderer->setResolver($resolver); + + $model = new ViewModel(); + $model->setTemplate('empty'); + + $content = $this->renderer->render($model); + $this->assertRegexp('/\s*Empty view\s*/s', $content); + } + + /** + * @group view-model + */ + public function testViewModelWithoutTemplateRaisesException() + { + $model = new ViewModel(); + $this->setExpectedException('Zend\View\Exception\DomainException'); + $content = $this->renderer->render($model); + } + + /** + * @group view-model + */ + public function testRendersViewModelWithVariablesSpecified() + { + $resolver = new TemplateMapResolver(array( + 'test' => __DIR__ . '/_templates/test.phtml', + )); + $this->renderer->setResolver($resolver); + + $model = new ViewModel(); + $model->setTemplate('test'); + $model->setVariable('bar', 'bar'); + + $content = $this->renderer->render($model); + $this->assertRegexp('/\s*foo bar baz\s*/s', $content); + } + + /** + * @group view-model + */ + public function testRenderedViewModelIsRegisteredAsCurrentViewModel() + { + $resolver = new TemplateMapResolver(array( + 'empty' => __DIR__ . '/_templates/empty.phtml', + )); + $this->renderer->setResolver($resolver); + + $model = new ViewModel(); + $model->setTemplate('empty'); + + $content = $this->renderer->render($model); + $helper = $this->renderer->plugin('view_model'); + $this->assertTrue($helper->hasCurrent()); + $this->assertSame($model, $helper->getCurrent()); + } + + public function testRendererRaisesExceptionIfResolverCannotResolveTemplate() + { + $expected = '10 > 9'; + $this->renderer->vars()->assign(array('foo' => '10 > 9')); + $this->setExpectedException('Zend\View\Exception\RuntimeException', 'could not resolve'); + $test = $this->renderer->render('should-not-find-this'); + } + + /** + * @group view-model + */ + public function testDoesNotRenderTreesOfViewModelsByDefault() + { + $this->assertFalse($this->renderer->canRenderTrees()); + } + + /** + * @group view-model + */ + public function testRenderTreesOfViewModelsCapabilityIsMutable() + { + $this->renderer->setCanRenderTrees(true); + $this->assertTrue($this->renderer->canRenderTrees()); + $this->renderer->setCanRenderTrees(false); + $this->assertFalse($this->renderer->canRenderTrees()); + } + + /** + * @group view-model + */ + public function testIfViewModelComposesVariablesInstanceThenRendererUsesIt() + { + $model = new ViewModel(); + $model->setTemplate('template'); + $vars = $model->getVariables(); + $vars['foo'] = 'BAR-BAZ-BAT'; + + $resolver = new TemplateMapResolver(array( + 'template' => __DIR__ . '/_templates/view-model-variables.phtml', + )); + $this->renderer->setResolver($resolver); + $test = $this->renderer->render($model); + $this->assertContains('BAR-BAZ-BAT', $test); + } +} diff --git a/test/Renderer/FeedRendererTest.php b/test/Renderer/FeedRendererTest.php new file mode 100644 index 00000000..eab1bfac --- /dev/null +++ b/test/Renderer/FeedRendererTest.php @@ -0,0 +1,128 @@ +renderer = new FeedRenderer(); + } + + protected function getFeedData($type) + { + return array( + 'copyright' => date('Y'), + 'date_created' => time(), + 'date_modified' => time(), + 'last_build_date' => time(), + 'description' => __CLASS__, + 'id' => 'http://framework.zend.com/', + 'language' => 'en_US', + 'feed_link' => array( + 'link' => 'http://framework.zend.com/feed.xml', + 'type' => $type, + ), + 'link' => 'http://framework.zend.com/feed.xml', + 'title' => 'Testing', + 'encoding' => 'UTF-8', + 'base_url' => 'http://framework.zend.com/', + 'entries' => array( + array( + 'content' => 'test content', + 'date_created' => time(), + 'date_modified' => time(), + 'description' => __CLASS__, + 'id' => 'http://framework.zend.com/1', + 'link' => 'http://framework.zend.com/1', + 'title' => 'Test 1', + ), + array( + 'content' => 'test content', + 'date_created' => time(), + 'date_modified' => time(), + 'description' => __CLASS__, + 'id' => 'http://framework.zend.com/2', + 'link' => 'http://framework.zend.com/2', + 'title' => 'Test 2', + ), + ), + ); + } + + public function testRendersFeedModelAccordingToTypeProvidedInModel() + { + $model = new FeedModel($this->getFeedData('atom')); + $model->setOption('feed_type', 'atom'); + $xml = $this->renderer->render($model); + $this->assertContains('<' . '?xml', $xml); + $this->assertContains('atom', $xml); + } + + public function testRendersFeedModelAccordingToRenderTypeIfNoTypeProvidedInModel() + { + $this->renderer->setFeedType('atom'); + $model = new FeedModel($this->getFeedData('atom')); + $xml = $this->renderer->render($model); + $this->assertContains('<' . '?xml', $xml); + $this->assertContains('atom', $xml); + } + + public function testCastsViewModelToFeedModelUsingFeedTypeOptionProvided() + { + $model = new ViewModel($this->getFeedData('atom')); + $model->setOption('feed_type', 'atom'); + $xml = $this->renderer->render($model); + $this->assertContains('<' . '?xml', $xml); + $this->assertContains('atom', $xml); + } + + public function testCastsViewModelToFeedModelUsingRendererFeedTypeIfNoFeedTypeOptionInModel() + { + $this->renderer->setFeedType('atom'); + $model = new ViewModel($this->getFeedData('atom')); + $xml = $this->renderer->render($model); + $this->assertContains('<' . '?xml', $xml); + $this->assertContains('atom', $xml); + } + + public function testStringModelWithValuesProvidedCastsToFeed() + { + $this->renderer->setFeedType('atom'); + $xml = $this->renderer->render('layout', $this->getFeedData('atom')); + $this->assertContains('<' . '?xml', $xml); + $this->assertContains('atom', $xml); + } + + public function testNonStringNonModelArgumentRaisesException() + { + $this->setExpectedException('Zend\View\Exception\InvalidArgumentException', 'expects'); + $this->renderer->render(array('foo')); + } + + public function testSettingUnacceptableFeedTypeRaisesException() + { + $this->setExpectedException('Zend\View\Exception\InvalidArgumentException', 'expects a string of either "rss" or "atom"'); + $this->renderer->setFeedType('foobar'); + } +} diff --git a/test/Renderer/JsonRendererTest.php b/test/Renderer/JsonRendererTest.php new file mode 100644 index 00000000..dcd60040 --- /dev/null +++ b/test/Renderer/JsonRendererTest.php @@ -0,0 +1,237 @@ +renderer = new JsonRenderer(); + } + + public function testRendersViewModelsWithoutChildren() + { + $model = new ViewModel(array('foo' => 'bar')); + $test = $this->renderer->render($model); + $this->assertEquals(json_encode(array('foo' => 'bar')), $test); + } + + public function testRendersViewModelsWithChildrenUsingCaptureToValue() + { + $root = new ViewModel(array('foo' => 'bar')); + $child1 = new ViewModel(array('foo' => 'bar')); + $child2 = new ViewModel(array('foo' => 'bar')); + $child1->setCaptureTo('child1'); + $child2->setCaptureTo('child2'); + $root->addChild($child1) + ->addChild($child2); + + $expected = array( + 'foo' => 'bar', + 'child1' => array( + 'foo' => 'bar', + ), + 'child2' => array( + 'foo' => 'bar', + ), + ); + $test = $this->renderer->render($root); + $this->assertEquals(json_encode($expected), $test); + } + + public function testThrowsAwayChildModelsWithoutCaptureToValueByDefault() + { + $root = new ViewModel(array('foo' => 'bar')); + $child1 = new ViewModel(array('foo' => 'baz')); + $child2 = new ViewModel(array('foo' => 'bar')); + $child1->setCaptureTo(false); + $child2->setCaptureTo('child2'); + $root->addChild($child1) + ->addChild($child2); + + $expected = array( + 'foo' => 'bar', + 'child2' => array( + 'foo' => 'bar', + ), + ); + $test = $this->renderer->render($root); + $this->assertEquals(json_encode($expected), $test); + } + + public function testCanMergeChildModelsWithoutCaptureToValues() + { + $this->renderer->setMergeUnnamedChildren(true); + $root = new ViewModel(array('foo' => 'bar')); + $child1 = new ViewModel(array('foo' => 'baz')); + $child2 = new ViewModel(array('foo' => 'bar')); + $child1->setCaptureTo(false); + $child2->setCaptureTo('child2'); + $root->addChild($child1) + ->addChild($child2); + + $expected = array( + 'foo' => 'baz', + 'child2' => array( + 'foo' => 'bar', + ), + ); + $test = $this->renderer->render($root); + $this->assertEquals(json_encode($expected), $test); + } + + public function getNonObjectModels() + { + return array( + array('string'), + array(1), + array(1.0), + array(array('foo', 'bar')), + array(array('foo' => 'bar')), + ); + } + + /** + * @dataProvider getNonObjectModels + */ + public function testRendersNonObjectModelAsJson($model) + { + $expected = json_encode($model); + $test = $this->renderer->render($model); + $this->assertEquals($expected, $test); + } + + public function testRendersJsonSerializableModelsAsJson() + { + if (version_compare(PHP_VERSION, '5.4.0', '<')) { + $this->markTestSkipped('Can only test JsonSerializable models in PHP 5.4.0 and up'); + } + $model = new TestAsset\JsonModel; + $model->value = array('foo' => 'bar'); + $expected = json_encode($model->value); + $test = $this->renderer->render($model); + $this->assertEquals($expected, $test); + } + + public function testRendersTraversableObjectsAsJsonObjects() + { + $model = new ArrayObject(array( + 'foo' => 'bar', + 'bar' => 'baz', + )); + $expected = json_encode($model->getArrayCopy()); + $test = $this->renderer->render($model); + $this->assertEquals($expected, $test); + } + + public function testRendersNonTraversableNonJsonSerializableObjectsAsJsonObjects() + { + $model = new stdClass; + $model->foo = 'bar'; + $model->bar = 'baz'; + $expected = json_encode(get_object_vars($model)); + $test = $this->renderer->render($model); + $this->assertEquals($expected, $test); + } + + public function testNonViewModelInitialArgumentWithValuesRaisesException() + { + $this->setExpectedException('Zend\View\Exception\DomainException'); + $this->renderer->render('foo', array('bar' => 'baz')); + } + + public function testRendersTreesOfViewModelsByDefault() + { + $this->assertTrue($this->renderer->canRenderTrees()); + } + + public function testSetHasJsonpCallback() + { + $this->assertFalse($this->renderer->hasJsonpCallback()); + $this->renderer->setJsonpCallback(0); + $this->assertFalse($this->renderer->hasJsonpCallback()); + $this->renderer->setJsonpCallback('callback'); + $this->assertTrue($this->renderer->hasJsonpCallback()); + } + + public function testRendersViewModelsWithoutChildrenWithJsonpCallback() + { + $model = new ViewModel(array('foo' => 'bar')); + $this->renderer->setJsonpCallback('callback'); + $test = $this->renderer->render($model); + $expected = 'callback(' . json_encode(array('foo' => 'bar')) . ');'; + $this->assertEquals($expected, $test); + } + + /** + * @dataProvider getNonObjectModels + */ + public function testRendersNonObjectModelAsJsonWithJsonpCallback($model) + { + $expected = 'callback(' . json_encode($model) . ');'; + $this->renderer->setJsonpCallback('callback'); + $test = $this->renderer->render($model); + $this->assertEquals($expected, $test); + } + + public function testRendersJsonSerializableModelsAsJsonWithJsonpCallback() + { + if (version_compare(PHP_VERSION, '5.4.0', '<')) { + $this->markTestSkipped('Can only test JsonSerializable models in PHP 5.4.0 and up'); + } + $model = new TestAsset\JsonModel; + $model->value = array('foo' => 'bar'); + $expected = 'callback(' . json_encode($model->value) . ');'; + $this->renderer->setJsonpCallback('callback'); + $test = $this->renderer->render($model); + $this->assertEquals($expected, $test); + } + + public function testRendersTraversableObjectsAsJsonObjectsWithJsonpCallback() + { + $model = new ArrayObject(array( + 'foo' => 'bar', + 'bar' => 'baz', + )); + $expected = 'callback(' . json_encode($model->getArrayCopy()) . ');'; + $this->renderer->setJsonpCallback('callback'); + $test = $this->renderer->render($model); + $this->assertEquals($expected, $test); + } + + public function testRendersNonTraversableNonJsonSerializableObjectsAsJsonObjectsWithJsonpCallback() + { + $model = new stdClass; + $model->foo = 'bar'; + $model->bar = 'baz'; + $expected = 'callback(' . json_encode(get_object_vars($model)) . ');'; + $this->renderer->setJsonpCallback('callback'); + $test = $this->renderer->render($model); + $this->assertEquals($expected, $test); + } +} diff --git a/test/Renderer/TestAsset/JsonModel.php b/test/Renderer/TestAsset/JsonModel.php new file mode 100644 index 00000000..8210bdb6 --- /dev/null +++ b/test/Renderer/TestAsset/JsonModel.php @@ -0,0 +1,28 @@ +value; + } +} diff --git a/test/Resolver/AggregateResolverTest.php b/test/Resolver/AggregateResolverTest.php new file mode 100644 index 00000000..fa773673 --- /dev/null +++ b/test/Resolver/AggregateResolverTest.php @@ -0,0 +1,134 @@ +assertEquals(0, count($resolver)); + } + + public function testCanAttachResolvers() + { + $resolver = new Resolver\AggregateResolver(); + $resolver->attach(new Resolver\TemplateMapResolver); + $this->assertEquals(1, count($resolver)); + $resolver->attach(new Resolver\TemplateMapResolver); + $this->assertEquals(2, count($resolver)); + } + + public function testReturnsNonFalseValueWhenAtLeastOneResolverSucceeds() + { + $resolver = new Resolver\AggregateResolver(); + $resolver->attach(new Resolver\TemplateMapResolver(array( + 'foo' => 'bar', + ))); + $resolver->attach(new Resolver\TemplateMapResolver(array( + 'bar' => 'baz', + ))); + $test = $resolver->resolve('bar'); + $this->assertEquals('baz', $test); + } + + public function testLastSuccessfulResolverIsNullInitially() + { + $resolver = new Resolver\AggregateResolver(); + $this->assertNull($resolver->getLastSuccessfulResolver()); + } + + public function testCanAccessResolverThatLastSucceeded() + { + $resolver = new Resolver\AggregateResolver(); + $fooResolver = new Resolver\TemplateMapResolver(array( + 'foo' => 'bar', + )); + $barResolver = new Resolver\TemplateMapResolver(array( + 'bar' => 'baz', + )); + $bazResolver = new Resolver\TemplateMapResolver(array( + 'baz' => 'bat', + )); + $resolver->attach($fooResolver) + ->attach($barResolver) + ->attach($bazResolver); + + $test = $resolver->resolve('bar'); + $this->assertEquals('baz', $test); + $this->assertSame($barResolver, $resolver->getLastSuccessfulResolver()); + } + + public function testReturnsFalseWhenNoResolverSucceeds() + { + $resolver = new Resolver\AggregateResolver(); + $resolver->attach(new Resolver\TemplateMapResolver(array( + 'foo' => 'bar', + ))); + $this->assertFalse($resolver->resolve('bar')); + $this->assertEquals(Resolver\AggregateResolver::FAILURE_NOT_FOUND, $resolver->getLastLookupFailure()); + } + + public function testLastSuccessfulResolverIsNullWhenNoResolverSucceeds() + { + $resolver = new Resolver\AggregateResolver(); + $fooResolver = new Resolver\TemplateMapResolver(array( + 'foo' => 'bar', + )); + $resolver->attach($fooResolver); + $test = $resolver->resolve('foo'); + $this->assertSame($fooResolver, $resolver->getLastSuccessfulResolver()); + + try { + $test = $resolver->resolve('bar'); + $this->fail('Should not have resolved!'); + } catch (\Exception $e) { + // exception is expected + } + $this->assertNull($resolver->getLastSuccessfulResolver()); + } + + public function testResolvesInOrderOfPriorityProvided() + { + $resolver = new Resolver\AggregateResolver(); + $fooResolver = new Resolver\TemplateMapResolver(array( + 'bar' => 'foo', + )); + $barResolver = new Resolver\TemplateMapResolver(array( + 'bar' => 'bar', + )); + $bazResolver = new Resolver\TemplateMapResolver(array( + 'bar' => 'baz', + )); + $resolver->attach($fooResolver, -1) + ->attach($barResolver, 100) + ->attach($bazResolver); + + $test = $resolver->resolve('bar'); + $this->assertEquals('bar', $test); + } + + public function testReturnsFalseWhenAttemptingToResolveWhenNoResolversAreAttached() + { + $resolver = new Resolver\AggregateResolver(); + $this->assertFalse($resolver->resolve('foo')); + $this->assertEquals(Resolver\AggregateResolver::FAILURE_NO_RESOLVERS, $resolver->getLastLookupFailure()); + } +} diff --git a/test/Resolver/TemplateMapResolverTest.php b/test/Resolver/TemplateMapResolverTest.php new file mode 100644 index 00000000..7c74ad1f --- /dev/null +++ b/test/Resolver/TemplateMapResolverTest.php @@ -0,0 +1,203 @@ +assertEquals(array(), $resolver->getMap()); + } + + public function testCanSeedMapWithArrayViaConstructor() + { + $map = array('foo/bar' => __DIR__ . '/foo/bar.phtml'); + $resolver = new TemplateMapResolver($map); + $this->assertEquals($map, $resolver->getMap()); + } + + public function testCanSeedMapWithTraversableViaConstructor() + { + $map = new ArrayObject(array('foo/bar' => __DIR__ . '/foo/bar.phtml')); + $resolver = new TemplateMapResolver($map); + $this->assertEquals($map->getArrayCopy(), $resolver->getMap()); + } + + public function testCanSeedMapWithArrayViaSetter() + { + $map = array('foo/bar' => __DIR__ . '/foo/bar.phtml'); + $resolver = new TemplateMapResolver(); + $resolver->setMap($map); + $this->assertEquals($map, $resolver->getMap()); + } + + public function testCanSeedMapWithTraversableViaSetter() + { + $map = new ArrayObject(array('foo/bar' => __DIR__ . '/foo/bar.phtml')); + $resolver = new TemplateMapResolver(); + $resolver->setMap($map); + $this->assertEquals($map->getArrayCopy(), $resolver->getMap()); + } + + public function testCanAppendSingleEntriesViaAdd() + { + $map = array('foo/bar' => __DIR__ . '/foo/bar.phtml'); + $resolver = new TemplateMapResolver($map); + $resolver->add('foo/baz', __DIR__ . '/../foo/baz.phtml'); + $expected = array_merge($map, array('foo/baz' => __DIR__ . '/../foo/baz.phtml')); + $this->assertEquals($expected, $resolver->getMap()); + } + + public function testCanAppendMultipleEntriesAsArrayViaAdd() + { + $map = array('foo/bar' => __DIR__ . '/foo/bar.phtml'); + $resolver = new TemplateMapResolver($map); + $more = array( + 'foo/baz' => __DIR__ . '/../foo/baz.phtml', + 'baz/bat' => __DIR__ . '/baz/bat.phtml', + ); + $resolver->add($more); + $expected = array_merge($map, $more); + $this->assertEquals($expected, $resolver->getMap()); + } + + public function testCanAppendMultipleEntriesAsTraversableViaAdd() + { + $map = array('foo/bar' => __DIR__ . '/foo/bar.phtml'); + $resolver = new TemplateMapResolver($map); + $more = new ArrayObject(array( + 'foo/baz' => __DIR__ . '/../foo/baz.phtml', + 'baz/bat' => __DIR__ . '/baz/bat.phtml', + )); + $resolver->add($more); + $expected = array_merge($map, $more->getArrayCopy()); + $this->assertEquals($expected, $resolver->getMap()); + } + + public function testCanAppendMultipleEntriesAsArrayViaMerge() + { + $map = array('foo/bar' => __DIR__ . '/foo/bar.phtml'); + $resolver = new TemplateMapResolver($map); + $more = array( + 'foo/baz' => __DIR__ . '/../foo/baz.phtml', + 'baz/bat' => __DIR__ . '/baz/bat.phtml', + ); + $resolver->merge($more); + $expected = array_merge($map, $more); + $this->assertEquals($expected, $resolver->getMap()); + } + + public function testCanAppendMultipleEntriesAsTraversableViaMerge() + { + $map = array('foo/bar' => __DIR__ . '/foo/bar.phtml'); + $resolver = new TemplateMapResolver($map); + $more = new ArrayObject(array( + 'foo/baz' => __DIR__ . '/../foo/baz.phtml', + 'baz/bat' => __DIR__ . '/baz/bat.phtml', + )); + $resolver->merge($more); + $expected = array_merge($map, $more->getArrayCopy()); + $this->assertEquals($expected, $resolver->getMap()); + } + + public function testCanMergeTwoMaps() + { + $map = array('foo/bar' => __DIR__ . '/foo/bar.phtml'); + $resolver = new TemplateMapResolver($map); + $more = new TemplateMapResolver(array( + 'foo/baz' => __DIR__ . '/../foo/baz.phtml', + 'baz/bat' => __DIR__ . '/baz/bat.phtml', + )); + $resolver->merge($more); + $expected = array_merge($map, $more->getMap()); + $this->assertEquals($expected, $resolver->getMap()); + } + + public function testAddOverwritesMatchingEntries() + { + $map = array('foo/bar' => __DIR__ . '/foo/bar.phtml'); + $resolver = new TemplateMapResolver($map); + $more = array( + 'foo/bar' => __DIR__ . '/../foo/baz.phtml', + 'baz/bat' => __DIR__ . '/baz/bat.phtml', + ); + $resolver->merge($more); + $expected = array_merge($map, $more); + $this->assertEquals($expected, $resolver->getMap()); + $this->assertEquals(__DIR__ . '/../foo/baz.phtml', $resolver->get('foo/bar')); + } + + public function testMergeOverwritesMatchingEntries() + { + $map = array('foo/bar' => __DIR__ . '/foo/bar.phtml'); + $resolver = new TemplateMapResolver($map); + $more = new TemplateMapResolver(array( + 'foo/bar' => __DIR__ . '/../foo/baz.phtml', + 'baz/bat' => __DIR__ . '/baz/bat.phtml', + )); + $resolver->merge($more); + $expected = array_merge($map, $more->getMap()); + $this->assertEquals($expected, $resolver->getMap()); + $this->assertEquals(__DIR__ . '/../foo/baz.phtml', $resolver->get('foo/bar')); + } + + public function testHasReturnsTrueWhenMatchingNameFound() + { + $map = array('foo/bar' => __DIR__ . '/foo/bar.phtml'); + $resolver = new TemplateMapResolver($map); + $this->assertTrue($resolver->has('foo/bar')); + } + + public function testHasReturnsFalseWhenNameHasNoMatch() + { + $map = array('foo/bar' => __DIR__ . '/foo/bar.phtml'); + $resolver = new TemplateMapResolver($map); + $this->assertFalse($resolver->has('bar/baz')); + } + + public function testGetReturnsPathWhenNameHasMatch() + { + $map = array('foo/bar' => __DIR__ . '/foo/bar.phtml'); + $resolver = new TemplateMapResolver($map); + $this->assertEquals($map['foo/bar'], $resolver->get('foo/bar')); + } + + public function testGetReturnsFalseWhenNameHasNoMatch() + { + $map = array('foo/bar' => __DIR__ . '/foo/bar.phtml'); + $resolver = new TemplateMapResolver($map); + $this->assertFalse($resolver->get('bar/baz')); + } + + public function testResolveReturnsPathWhenNameHasMatch() + { + $map = array('foo/bar' => __DIR__ . '/foo/bar.phtml'); + $resolver = new TemplateMapResolver($map); + $this->assertEquals($map['foo/bar'], $resolver->resolve('foo/bar')); + } + + public function testResolveReturnsFalseWhenNameHasNoMatch() + { + $map = array('foo/bar' => __DIR__ . '/foo/bar.phtml'); + $resolver = new TemplateMapResolver($map); + $this->assertFalse($resolver->resolve('bar/baz')); + } +} diff --git a/test/Strategy/FeedStrategyTest.php b/test/Strategy/FeedStrategyTest.php new file mode 100644 index 00000000..7d8cc70f --- /dev/null +++ b/test/Strategy/FeedStrategyTest.php @@ -0,0 +1,263 @@ +renderer = new FeedRenderer; + $this->strategy = new FeedStrategy($this->renderer); + $this->event = new ViewEvent(); + $this->response = new HttpResponse(); + } + + public function testFeedModelSelectsFeedStrategy() + { + $this->event->setModel(new FeedModel()); + $result = $this->strategy->selectRenderer($this->event); + $this->assertSame($this->renderer, $result); + } + + public function testRssAcceptHeaderSelectsFeedStrategy() + { + $request = new HttpRequest(); + $request->getHeaders()->addHeaderLine('Accept', 'application/rss+xml'); + $this->event->setRequest($request); + $result = $this->strategy->selectRenderer($this->event); + $this->assertSame($this->renderer, $result); + } + + public function testAtomAcceptHeaderSelectsFeedStrategy() + { + $request = new HttpRequest(); + $request->getHeaders()->addHeaderLine('Accept', 'application/atom+xml'); + $this->event->setRequest($request); + $result = $this->strategy->selectRenderer($this->event); + $this->assertSame($this->renderer, $result); + } + + public function testLackOfFeedModelOrAcceptHeaderDoesNotSelectFeedStrategy() + { + $result = $this->strategy->selectRenderer($this->event); + $this->assertNotSame($this->renderer, $result); + $this->assertNull($result); + } + + protected function assertResponseNotInjected() + { + $content = $this->response->getContent(); + $headers = $this->response->getHeaders(); + $this->assertTrue(empty($content)); + $this->assertFalse($headers->has('content-type')); + } + + public function testNonMatchingRendererDoesNotInjectResponse() + { + $this->event->setResponse($this->response); + + // test empty renderer + $this->strategy->injectResponse($this->event); + $this->assertResponseNotInjected(); + + // test non-matching renderer + $renderer = new FeedRenderer(); + $this->event->setRenderer($renderer); + $this->strategy->injectResponse($this->event); + $this->assertResponseNotInjected(); + } + + public function testNonStringOrFeedResultDoesNotInjectResponse() + { + $this->event->setResponse($this->response); + $this->event->setRenderer($this->renderer); + $this->event->setResult($this->response); + + $this->strategy->injectResponse($this->event); + $this->assertResponseNotInjected(); + } + + public function testMatchingRendererAndStringResultInjectsResponse() + { + $this->renderer->setFeedType('atom'); + $expected = 'content'; + $this->event->setResponse($this->response); + $this->event->setRenderer($this->renderer); + $this->event->setResult($expected); + + $this->strategy->injectResponse($this->event); + $content = $this->response->getContent(); + $headers = $this->response->getHeaders(); + $this->assertEquals($expected, $content); + $this->assertTrue($headers->has('content-type')); + $this->assertEquals('application/atom+xml', $headers->get('content-type')->getFieldValue()); + } + + protected function getFeedData($type) + { + return array( + 'copyright' => date('Y'), + 'date_created' => time(), + 'date_modified' => time(), + 'last_build_date' => time(), + 'description' => __CLASS__, + 'id' => 'http://framework.zend.com/', + 'language' => 'en_US', + 'feed_link' => array( + 'link' => 'http://framework.zend.com/feed.xml', + 'type' => $type, + ), + 'link' => 'http://framework.zend.com/feed.xml', + 'title' => 'Testing', + 'encoding' => 'UTF-8', + 'base_url' => 'http://framework.zend.com/', + 'entries' => array( + array( + 'content' => 'test content', + 'date_created' => time(), + 'date_modified' => time(), + 'description' => __CLASS__, + 'id' => 'http://framework.zend.com/1', + 'link' => 'http://framework.zend.com/1', + 'title' => 'Test 1', + ), + array( + 'content' => 'test content', + 'date_created' => time(), + 'date_modified' => time(), + 'description' => __CLASS__, + 'id' => 'http://framework.zend.com/2', + 'link' => 'http://framework.zend.com/2', + 'title' => 'Test 2', + ), + ), + ); + } + + public function testMatchingRendererAndFeedResultInjectsResponse() + { + $this->renderer->setFeedType('atom'); + $expected = FeedFactory::factory($this->getFeedData('atom')); + $this->event->setResponse($this->response); + $this->event->setRenderer($this->renderer); + $this->event->setResult($expected); + + $this->strategy->injectResponse($this->event); + $content = $this->response->getContent(); + $headers = $this->response->getHeaders(); + $this->assertEquals($expected->export('atom'), $content); + $this->assertTrue($headers->has('content-type')); + $this->assertEquals('application/atom+xml', $headers->get('content-type')->getFieldValue()); + } + + public function testResponseContentTypeIsBasedOnFeedType() + { + $this->renderer->setFeedType('rss'); + $expected = FeedFactory::factory($this->getFeedData('rss')); + $this->event->setResponse($this->response); + $this->event->setRenderer($this->renderer); + $this->event->setResult($expected); + + $this->strategy->injectResponse($this->event); + $content = $this->response->getContent(); + $headers = $this->response->getHeaders(); + $this->assertEquals($expected->export('rss'), $content); + $this->assertTrue($headers->has('content-type')); + $this->assertEquals('application/rss+xml', $headers->get('content-type')->getFieldValue()); + } + + public function testReturnsNullWhenUnableToSelectRenderer() + { + $model = new ViewModel(); + $request = new HttpRequest(); + $this->event->setModel($model); + $this->event->setRequest($request); + $this->assertNull($this->strategy->selectRenderer($this->event)); + } + + public function testAttachesListenersAtExpectedPriorities() + { + $events = new EventManager(); + $events->attachAggregate($this->strategy); + + foreach (array('renderer' => 'selectRenderer', 'response' => 'injectResponse') as $event => $method) { + $listeners = $events->getListeners($event); + $expectedCallback = array($this->strategy, $method); + $expectedPriority = 1; + $found = false; + foreach ($listeners as $listener) { + $callback = $listener->getCallback(); + if ($callback === $expectedCallback) { + if ($listener->getMetadatum('priority') == $expectedPriority) { + $found = true; + break; + } + } + } + $this->assertTrue($found, 'Listener not found'); + } + } + + public function testCanAttachListenersAtSpecifiedPriority() + { + $events = new EventManager(); + $events->attachAggregate($this->strategy, 100); + + foreach (array('renderer' => 'selectRenderer', 'response' => 'injectResponse') as $event => $method) { + $listeners = $events->getListeners($event); + $expectedCallback = array($this->strategy, $method); + $expectedPriority = 100; + $found = false; + foreach ($listeners as $listener) { + $callback = $listener->getCallback(); + if ($callback === $expectedCallback) { + if ($listener->getMetadatum('priority') == $expectedPriority) { + $found = true; + break; + } + } + } + $this->assertTrue($found, 'Listener not found'); + } + } + + public function testDetachesListeners() + { + $events = new EventManager(); + $events->attachAggregate($this->strategy); + $listeners = $events->getListeners('renderer'); + $this->assertEquals(1, count($listeners)); + $listeners = $events->getListeners('response'); + $this->assertEquals(1, count($listeners)); + $events->detachAggregate($this->strategy); + $listeners = $events->getListeners('renderer'); + $this->assertEquals(0, count($listeners)); + $listeners = $events->getListeners('response'); + $this->assertEquals(0, count($listeners)); + } +} diff --git a/test/Strategy/JsonStrategyTest.php b/test/Strategy/JsonStrategyTest.php new file mode 100644 index 00000000..cf2cce6b --- /dev/null +++ b/test/Strategy/JsonStrategyTest.php @@ -0,0 +1,218 @@ +renderer = new JsonRenderer; + $this->strategy = new JsonStrategy($this->renderer); + $this->event = new ViewEvent(); + $this->response = new HttpResponse(); + } + + public function testJsonModelSelectsJsonStrategy() + { + $this->event->setModel(new JsonModel()); + $result = $this->strategy->selectRenderer($this->event); + $this->assertSame($this->renderer, $result); + } + + public function testJsonAcceptHeaderSelectsJsonStrategy() + { + $request = new HttpRequest(); + $request->getHeaders()->addHeaderLine('Accept', 'application/json'); + $this->event->setRequest($request); + $result = $this->strategy->selectRenderer($this->event); + $this->assertSame($this->renderer, $result); + } + + public function testJavascriptAcceptHeaderSelectsJsonStrategy() + { + $request = new HttpRequest(); + $request->getHeaders()->addHeaderLine('Accept', 'application/javascript'); + $this->event->setRequest($request); + $result = $this->strategy->selectRenderer($this->event); + $this->assertSame($this->renderer, $result); + $this->assertFalse($result->hasJsonpCallback()); + } + + public function testJavascriptAcceptHeaderSelectsJsonStrategyAndSetsJsonpCallback() + { + $request = new HttpRequest(); + $request->getHeaders()->addHeaderLine('Accept', 'application/javascript'); + $request->setQuery(new Parameters(array('callback' => 'foo'))); + $this->event->setRequest($request); + $result = $this->strategy->selectRenderer($this->event); + $this->assertSame($this->renderer, $result); + $this->assertTrue($result->hasJsonpCallback()); + } + + public function testLackOfJsonModelOrAcceptHeaderDoesNotSelectJsonStrategy() + { + $result = $this->strategy->selectRenderer($this->event); + $this->assertNotSame($this->renderer, $result); + $this->assertNull($result); + } + + protected function assertResponseNotInjected() + { + $content = $this->response->getContent(); + $headers = $this->response->getHeaders(); + $this->assertTrue(empty($content)); + $this->assertFalse($headers->has('content-type')); + } + + public function testNonMatchingRendererDoesNotInjectResponse() + { + $this->event->setResponse($this->response); + + // test empty renderer + $this->strategy->injectResponse($this->event); + $this->assertResponseNotInjected(); + + // test non-matching renderer + $renderer = new JsonRenderer(); + $this->event->setRenderer($renderer); + $this->strategy->injectResponse($this->event); + $this->assertResponseNotInjected(); + } + + public function testNonStringResultDoesNotInjectResponse() + { + $this->event->setResponse($this->response); + $this->event->setRenderer($this->renderer); + $this->event->setResult($this->response); + + $this->strategy->injectResponse($this->event); + $this->assertResponseNotInjected(); + } + + public function testMatchingRendererAndStringResultInjectsResponse() + { + $expected = json_encode(array('foo' => 'bar')); + $this->event->setResponse($this->response); + $this->event->setRenderer($this->renderer); + $this->event->setResult($expected); + + $this->strategy->injectResponse($this->event); + $content = $this->response->getContent(); + $headers = $this->response->getHeaders(); + $this->assertEquals($expected, $content); + $this->assertTrue($headers->has('content-type')); + $this->assertEquals('application/json', $headers->get('content-type')->getFieldValue()); + } + + public function testMatchingRendererAndStringResultInjectsResponseJsonp() + { + $expected = json_encode(array('foo' => 'bar')); + $this->renderer->setJsonpCallback('foo'); + $this->event->setResponse($this->response); + $this->event->setRenderer($this->renderer); + $this->event->setResult($expected); + + $this->strategy->injectResponse($this->event); + $content = $this->response->getContent(); + $headers = $this->response->getHeaders(); + $this->assertEquals($expected, $content); + $this->assertTrue($headers->has('content-type')); + $this->assertEquals('application/javascript', $headers->get('content-type')->getFieldValue()); + } + + public function testReturnsNullWhenCannotSelectRenderer() + { + $model = new ViewModel(); + $request = new HttpRequest(); + $this->event->setModel($model); + $this->event->setRequest($request); + + $this->assertNull($this->strategy->selectRenderer($this->event)); + } + + public function testAttachesListenersAtExpectedPriorities() + { + $events = new EventManager(); + $events->attachAggregate($this->strategy); + + foreach (array('renderer' => 'selectRenderer', 'response' => 'injectResponse') as $event => $method) { + $listeners = $events->getListeners($event); + $expectedCallback = array($this->strategy, $method); + $expectedPriority = 1; + $found = false; + foreach ($listeners as $listener) { + $callback = $listener->getCallback(); + if ($callback === $expectedCallback) { + if ($listener->getMetadatum('priority') == $expectedPriority) { + $found = true; + break; + } + } + } + $this->assertTrue($found, 'Listener not found'); + } + } + + public function testCanAttachListenersAtSpecifiedPriority() + { + $events = new EventManager(); + $events->attachAggregate($this->strategy, 1000); + + foreach (array('renderer' => 'selectRenderer', 'response' => 'injectResponse') as $event => $method) { + $listeners = $events->getListeners($event); + $expectedCallback = array($this->strategy, $method); + $expectedPriority = 1000; + $found = false; + foreach ($listeners as $listener) { + $callback = $listener->getCallback(); + if ($callback === $expectedCallback) { + if ($listener->getMetadatum('priority') == $expectedPriority) { + $found = true; + break; + } + } + } + $this->assertTrue($found, 'Listener not found'); + } + } + + public function testDetachesListeners() + { + $events = new EventManager(); + $events->attachAggregate($this->strategy); + $listeners = $events->getListeners('renderer'); + $this->assertEquals(1, count($listeners)); + $listeners = $events->getListeners('response'); + $this->assertEquals(1, count($listeners)); + $events->detachAggregate($this->strategy); + $listeners = $events->getListeners('renderer'); + $this->assertEquals(0, count($listeners)); + $listeners = $events->getListeners('response'); + $this->assertEquals(0, count($listeners)); + } +} diff --git a/test/Strategy/PhpRendererStrategyTest.php b/test/Strategy/PhpRendererStrategyTest.php new file mode 100644 index 00000000..ac5dc30f --- /dev/null +++ b/test/Strategy/PhpRendererStrategyTest.php @@ -0,0 +1,180 @@ +renderer = new PhpRenderer; + $this->strategy = new PhpRendererStrategy($this->renderer); + $this->event = new ViewEvent(); + $this->response = new HttpResponse(); + } + + public function testSelectRendererAlwaysSelectsPhpRenderer() + { + $result = $this->strategy->selectRenderer($this->event); + $this->assertSame($this->renderer, $result); + } + + protected function assertResponseNotInjected() + { + $content = $this->response->getContent(); + $headers = $this->response->getHeaders(); + $this->assertTrue(empty($content)); + $this->assertFalse($headers->has('content-type')); + } + + public function testNonMatchingRendererDoesNotInjectResponse() + { + $this->event->setResponse($this->response); + + // test empty renderer + $this->strategy->injectResponse($this->event); + $this->assertResponseNotInjected(); + + // test non-matching renderer + $renderer = new PhpRenderer(); + $this->event->setRenderer($renderer); + $this->strategy->injectResponse($this->event); + $this->assertResponseNotInjected(); + } + + public function testResponseContentSetToContentPlaceholderWhenResultAndArticlePlaceholderAreEmpty() + { + $this->renderer->placeholder('content')->set('Content'); + $event = new ViewEvent(); + $event->setResponse($this->response) + ->setRenderer($this->renderer); + + $this->strategy->injectResponse($event); + $content = $this->response->getContent(); + $this->assertEquals('Content', $content); + } + + public function testResponseContentSetToArticlePlaceholderWhenResultIsEmptyAndBothArticleAndContentPlaceholdersSet() + { + $this->renderer->placeholder('article')->set('Article Content'); + $this->renderer->placeholder('content')->set('Content'); + $event = new ViewEvent(); + $event->setResponse($this->response) + ->setRenderer($this->renderer); + + $this->strategy->injectResponse($event); + $content = $this->response->getContent(); + $this->assertEquals('Article Content', $content); + } + + public function testResponseContentSetToResultIfNotEmpty() + { + $this->renderer->placeholder('article')->set('Article Content'); + $this->renderer->placeholder('content')->set('Content'); + $event = new ViewEvent(); + $event->setResponse($this->response) + ->setRenderer($this->renderer) + ->setResult('Result Content'); + + $this->strategy->injectResponse($event); + $content = $this->response->getContent(); + $this->assertEquals('Result Content', $content); + } + + public function testContentPlaceholdersIncludeContentAndArticleByDefault() + { + $this->assertEquals(array('article', 'content'), $this->strategy->getContentPlaceholders()); + } + + public function testContentPlaceholdersListIsMutable() + { + $this->strategy->setContentPlaceholders(array('foo', 'bar')); + $this->assertEquals(array('foo', 'bar'), $this->strategy->getContentPlaceholders()); + } + + public function testAttachesListenersAtExpectedPriorities() + { + $events = new EventManager(); + $events->attachAggregate($this->strategy); + + foreach (array('renderer' => 'selectRenderer', 'response' => 'injectResponse') as $event => $method) { + $listeners = $events->getListeners($event); + $expectedCallback = array($this->strategy, $method); + $expectedPriority = 1; + $found = false; + foreach ($listeners as $listener) { + $callback = $listener->getCallback(); + if ($callback === $expectedCallback) { + if ($listener->getMetadatum('priority') == $expectedPriority) { + $found = true; + break; + } + } + } + $this->assertTrue($found, 'Listener not found'); + } + } + + public function testCanAttachListenersAtSpecifiedPriority() + { + $events = new EventManager(); + $events->attachAggregate($this->strategy, 100); + + foreach (array('renderer' => 'selectRenderer', 'response' => 'injectResponse') as $event => $method) { + $listeners = $events->getListeners($event); + $expectedCallback = array($this->strategy, $method); + $expectedPriority = 100; + $found = false; + foreach ($listeners as $listener) { + $callback = $listener->getCallback(); + if ($callback === $expectedCallback) { + if ($listener->getMetadatum('priority') == $expectedPriority) { + $found = true; + break; + } + } + } + $this->assertTrue($found, 'Listener not found'); + } + } + + public function testDetachesListeners() + { + $events = new EventManager(); + $events->attachAggregate($this->strategy); + $listeners = $events->getListeners('renderer'); + $this->assertEquals(1, count($listeners)); + $listeners = $events->getListeners('response'); + $this->assertEquals(1, count($listeners)); + $events->detachAggregate($this->strategy); + $listeners = $events->getListeners('renderer'); + $this->assertEquals(0, count($listeners)); + $listeners = $events->getListeners('response'); + $this->assertEquals(0, count($listeners)); + } +} diff --git a/test/TemplatePathStackTest.php b/test/TemplatePathStackTest.php new file mode 100644 index 00000000..d277a0f3 --- /dev/null +++ b/test/TemplatePathStackTest.php @@ -0,0 +1,256 @@ +stack = new TemplatePathStack(); + $this->paths = array( + TemplatePathStack::normalizePath(__DIR__), + TemplatePathStack::normalizePath(__DIR__ . '/_templates'), + ); + } + + public function testAddPathAddsPathToStack() + { + $this->stack->addPath(__DIR__); + $paths = $this->stack->getPaths(); + $this->assertEquals(1, count($paths)); + $this->assertEquals(TemplatePathStack::normalizePath(__DIR__), $paths->pop()); + } + + public function testPathsAreProcessedAsStack() + { + $paths = array( + TemplatePathStack::normalizePath(__DIR__), + TemplatePathStack::normalizePath(__DIR__ . '/_files'), + ); + foreach ($paths as $path) { + $this->stack->addPath($path); + } + $test = $this->stack->getPaths()->toArray(); + $this->assertEquals(array_reverse($paths), $test); + } + + public function testAddPathsAddsPathsToStack() + { + $this->stack->addPath(__DIR__ . '/Helper'); + $paths = array( + TemplatePathStack::normalizePath(__DIR__), + TemplatePathStack::normalizePath(__DIR__ . '/_files'), + ); + $this->stack->addPaths($paths); + array_unshift($paths, TemplatePathStack::normalizePath(__DIR__ . '/Helper')); + $this->assertEquals(array_reverse($paths), $this->stack->getPaths()->toArray()); + } + + public function testSetPathsOverwritesStack() + { + $this->stack->addPath(__DIR__ . '/Helper'); + $paths = array( + TemplatePathStack::normalizePath(__DIR__), + TemplatePathStack::normalizePath(__DIR__ . '/_files'), + ); + $this->stack->setPaths($paths); + $this->assertEquals(array_reverse($paths), $this->stack->getPaths()->toArray()); + } + + public function testClearPathsClearsStack() + { + $paths = array( + __DIR__, + __DIR__ . '/_files', + ); + $this->stack->setPaths($paths); + $this->stack->clearPaths(); + $this->assertEquals(0, $this->stack->getPaths()->count()); + } + + public function testLfiProtectionEnabledByDefault() + { + $this->assertTrue($this->stack->isLfiProtectionOn()); + } + + public function testMayDisableLfiProtection() + { + $this->stack->setLfiProtection(false); + $this->assertFalse($this->stack->isLfiProtectionOn()); + } + + public function testStreamWrapperDisabledByDefault() + { + $this->assertFalse($this->stack->useStreamWrapper()); + } + + public function testMayEnableStreamWrapper() + { + $flag = (bool) ini_get('short_open_tag'); + if (!$flag) { + $this->markTestSkipped('Short tags are disabled; cannot test'); + } + $this->stack->setUseStreamWrapper(true); + $this->assertTrue($this->stack->useStreamWrapper()); + } + + public function testDoesNotAllowParentDirectoryTraversalByDefault() + { + $this->stack->addPath(__DIR__ . '/_templates'); + + $this->setExpectedException('Zend\View\Exception\ExceptionInterface', 'parent directory traversal'); + $test = $this->stack->resolve('../_stubs/scripts/LfiProtectionCheck.phtml'); + } + + public function testDisablingLfiProtectionAllowsParentDirectoryTraversal() + { + $this->stack->setLfiProtection(false) + ->addPath(__DIR__ . '/_templates'); + + $test = $this->stack->resolve('../_stubs/scripts/LfiProtectionCheck.phtml'); + $this->assertContains('LfiProtectionCheck.phtml', $test); + } + + public function testReturnsFalseWhenRetrievingScriptIfNoPathsRegistered() + { + $this->assertFalse($this->stack->resolve('test.phtml')); + $this->assertEquals(TemplatePathStack::FAILURE_NO_PATHS, $this->stack->getLastLookupFailure()); + } + + public function testReturnsFalseWhenUnableToResolveScriptToPath() + { + $this->stack->addPath(__DIR__ . '/_templates'); + $this->assertFalse($this->stack->resolve('bogus-script.txt')); + $this->assertEquals(TemplatePathStack::FAILURE_NOT_FOUND, $this->stack->getLastLookupFailure()); + } + + public function testReturnsFullPathNameWhenAbleToResolveScriptPath() + { + $this->stack->addPath(__DIR__ . '/_templates'); + $expected = realpath(__DIR__ . '/_templates/test.phtml'); + $test = $this->stack->resolve('test.phtml'); + $this->assertEquals($expected, $test); + } + + public function testReturnsPathWithStreamProtocolWhenStreamWrapperEnabled() + { + $flag = (bool) ini_get('short_open_tag'); + if (!$flag) { + $this->markTestSkipped('Short tags are disabled; cannot test'); + } + $this->stack->setUseStreamWrapper(true) + ->addPath(__DIR__ . '/_templates'); + $expected = 'zend.view://' . realpath(__DIR__ . '/_templates/test.phtml'); + $test = $this->stack->resolve('test.phtml'); + $this->assertEquals($expected, $test); + } + + public function invalidOptions() + { + return array( + array(true), + array(1), + array(1.0), + array('foo'), + array(new \stdClass), + ); + } + + /** + * @dataProvider invalidOptions + */ + public function testSettingOptionsWithInvalidArgumentRaisesException($arg) + { + $this->setExpectedException('Zend\View\Exception\ExceptionInterface'); + $this->stack->setOptions($arg); + } + + public function validOptions() + { + $options = array( + 'lfi_protection' => false, + 'use_stream_wrapper' => true, + ); + return array( + array($options), + array(new \ArrayObject($options)), + ); + } + + /** + * @dataProvider validOptions + */ + public function testAllowsSettingOptions($arg) + { + $arg['script_paths'] = $this->paths; + $this->stack->setOptions($arg); + $this->assertFalse($this->stack->isLfiProtectionOn()); + + $expected = (bool) ini_get('short_open_tag'); + $this->assertSame($expected, $this->stack->useStreamWrapper()); + + $this->assertEquals(array_reverse($this->paths), $this->stack->getPaths()->toArray()); + } + + /** + * @dataProvider validOptions + */ + public function testAllowsPassingOptionsToConstructor($arg) + { + $arg['script_paths'] = $this->paths; + $stack = new TemplatePathStack($arg); + $this->assertFalse($stack->isLfiProtectionOn()); + + $expected = (bool) ini_get('short_open_tag'); + $this->assertSame($expected, $stack->useStreamWrapper()); + + $this->assertEquals(array_reverse($this->paths), $stack->getPaths()->toArray()); + } + + public function testAllowsRelativePharPath() + { + $path = 'phar://' . __DIR__ + . DIRECTORY_SEPARATOR . '_templates' + . DIRECTORY_SEPARATOR . 'view.phar' + . DIRECTORY_SEPARATOR . 'start' + . DIRECTORY_SEPARATOR . '..' + . DIRECTORY_SEPARATOR . 'views'; + + $this->stack->addPath($path); + $test = $this->stack->resolve('foo' . DIRECTORY_SEPARATOR . 'hello.phtml'); + $this->assertEquals($path . DIRECTORY_SEPARATOR . 'foo' . DIRECTORY_SEPARATOR . 'hello.phtml', $test); + } + + public function testDefaultFileSuffixIsPhtml() + { + $this->assertEquals('phtml', $this->stack->getDefaultSuffix()); + } + + public function testDefaultFileSuffixIsMutable() + { + $this->stack->setDefaultSuffix('php'); + $this->assertEquals('php', $this->stack->getDefaultSuffix()); + } + + public function testSettingDefaultSuffixStripsLeadingDot() + { + $this->stack->setDefaultSuffix('.config.php'); + $this->assertEquals('config.php', $this->stack->getDefaultSuffix()); + } +} diff --git a/test/TestAsset/Invokable.php b/test/TestAsset/Invokable.php new file mode 100644 index 00000000..3c8aa72d --- /dev/null +++ b/test/TestAsset/Invokable.php @@ -0,0 +1,28 @@ +getVariables(); + return var_export($values, true); + } +} diff --git a/test/TestAsset/Uninvokable.php b/test/TestAsset/Uninvokable.php new file mode 100644 index 00000000..3dbfbf6d --- /dev/null +++ b/test/TestAsset/Uninvokable.php @@ -0,0 +1,17 @@ +value = $value; + } + + public function __invoke() + { + return $this->value; + } +} diff --git a/test/VariablesTest.php b/test/VariablesTest.php new file mode 100644 index 00000000..8f03b849 --- /dev/null +++ b/test/VariablesTest.php @@ -0,0 +1,128 @@ +error = false; + $this->vars = new Variables; + } + + public function testStrictVarsAreDisabledByDefault() + { + $this->assertFalse($this->vars->isStrict()); + } + + public function testCanSetStrictFlag() + { + $this->vars->setStrictVars(true); + $this->assertTrue($this->vars->isStrict()); + } + + public function testAssignMergesValuesWithObject() + { + $this->vars['foo'] = 'bar'; + $this->vars->assign(array( + 'bar' => 'baz', + 'baz' => 'foo', + )); + $this->assertEquals('bar', $this->vars['foo']); + $this->assertEquals('baz', $this->vars['bar']); + $this->assertEquals('foo', $this->vars['baz']); + } + + public function testAssignCastsPlainObjectToArrayBeforeMerging() + { + $vars = new \stdClass; + $vars->foo = 'bar'; + $vars->bar = 'baz'; + + $this->vars->assign($vars); + $this->assertEquals('bar', $this->vars['foo']); + $this->assertEquals('baz', $this->vars['bar']); + } + + public function testAssignCallsToArrayWhenPresentBeforeMerging() + { + $vars = array( + 'foo' => 'bar', + 'bar' => 'baz', + ); + $config = new Config($vars); + $this->vars->assign($config); + $this->assertEquals('bar', $this->vars['foo']); + $this->assertEquals('baz', $this->vars['bar']); + } + + public function testNullIsReturnedForUndefinedVariables() + { + $this->assertNull($this->vars['foo']); + } + + public function handleErrors($errcode, $errmsg) + { + $this->error = $errmsg; + } + + public function testRetrievingUndefinedVariableRaisesErrorWhenStrictVarsIsRequested() + { + $this->vars->setStrictVars(true); + set_error_handler(array($this, 'handleErrors'), E_USER_NOTICE); + $this->assertNull($this->vars['foo']); + restore_error_handler(); + $this->assertContains('does not exist', $this->error); + } + + public function values() + { + return array( + array('foo', 'bar'), + array('xss', '\'value\''), + ); + } + + public function testCallingClearEmptiesObject() + { + $this->vars->assign(array( + 'bar' => 'baz', + 'baz' => 'foo', + )); + $this->assertEquals(2, count($this->vars)); + $this->vars->clear(); + $this->assertEquals(0, count($this->vars)); + } + + public function testAllowsSpecifyingClosureValuesAndReturningTheValue() + { + $this->vars->foo = function () { + return 'bar'; + }; + + $this->assertEquals('bar', $this->vars->foo); + } + + public function testAllowsSpecifyingFunctorValuesAndReturningTheValue() + { + $this->vars->foo = new TestAsset\VariableFunctor('bar'); + $this->assertEquals('bar', $this->vars->foo); + } +} diff --git a/test/ViewEventTest.php b/test/ViewEventTest.php new file mode 100644 index 00000000..65ff0104 --- /dev/null +++ b/test/ViewEventTest.php @@ -0,0 +1,166 @@ +event = new ViewEvent; + } + + public function testModelIsNullByDefault() + { + $this->assertNull($this->event->getModel()); + } + + public function testRendererIsNullByDefault() + { + $this->assertNull($this->event->getRenderer()); + } + + public function testRequestIsNullByDefault() + { + $this->assertNull($this->event->getRequest()); + } + + public function testResponseIsNullByDefault() + { + $this->assertNull($this->event->getResponse()); + } + + public function testResultIsNullByDefault() + { + $this->assertNull($this->event->getResult()); + } + + public function testModelIsMutable() + { + $model = new ViewModel(); + $this->event->setModel($model); + $this->assertSame($model, $this->event->getModel()); + } + + public function testRendererIsMutable() + { + $renderer = new PhpRenderer(); + $this->event->setRenderer($renderer); + $this->assertSame($renderer, $this->event->getRenderer()); + } + + public function testRequestIsMutable() + { + $request = new Request(); + $this->event->setRequest($request); + $this->assertSame($request, $this->event->getRequest()); + } + + public function testResponseIsMutable() + { + $response = new Response(); + $this->event->setResponse($response); + $this->assertSame($response, $this->event->getResponse()); + } + + public function testResultIsMutable() + { + $result = 'some result'; + $this->event->setResult($result); + $this->assertSame($result, $this->event->getResult()); + } + + public function testModelIsMutableViaSetParam() + { + $model = new ViewModel(); + $this->event->setParam('model', $model); + $this->assertSame($model, $this->event->getModel()); + $this->assertSame($model, $this->event->getParam('model')); + } + + public function testRendererIsMutableViaSetParam() + { + $renderer = new PhpRenderer(); + $this->event->setParam('renderer', $renderer); + $this->assertSame($renderer, $this->event->getRenderer()); + $this->assertSame($renderer, $this->event->getParam('renderer')); + } + + public function testRequestIsMutableViaSetParam() + { + $request = new Request(); + $this->event->setParam('request', $request); + $this->assertSame($request, $this->event->getRequest()); + $this->assertSame($request, $this->event->getParam('request')); + } + + public function testResponseIsMutableViaSetParam() + { + $response = new Response(); + $this->event->setParam('response', $response); + $this->assertSame($response, $this->event->getResponse()); + $this->assertSame($response, $this->event->getParam('response')); + } + + public function testResultIsMutableViaSetParam() + { + $result = 'some result'; + $this->event->setParam('result', $result); + $this->assertSame($result, $this->event->getResult()); + $this->assertSame($result, $this->event->getParam('result')); + } + + public function testSpecializedParametersMayBeSetViaSetParams() + { + $model = new ViewModel(); + $renderer = new PhpRenderer(); + $request = new Request(); + $response = new Response(); + $result = 'some result'; + + $params = array( + 'model' => $model, + 'renderer' => $renderer, + 'request' => $request, + 'response' => $response, + 'result' => $result, + 'otherkey' => 'other value', + ); + + $this->event->setParams($params); + $this->assertEquals($params, $this->event->getParams()); + + $this->assertSame($params['model'], $this->event->getModel()); + $this->assertSame($params['model'], $this->event->getParam('model')); + + $this->assertSame($params['renderer'], $this->event->getRenderer()); + $this->assertSame($params['renderer'], $this->event->getParam('renderer')); + + $this->assertSame($params['request'], $this->event->getRequest()); + $this->assertSame($params['request'], $this->event->getParam('request')); + + $this->assertSame($params['response'], $this->event->getResponse()); + $this->assertSame($params['response'], $this->event->getParam('response')); + + $this->assertSame($params['result'], $this->event->getResult()); + $this->assertSame($params['result'], $this->event->getParam('result')); + + $this->assertEquals($params['otherkey'], $this->event->getParam('otherkey')); + } +} diff --git a/test/ViewTest.php b/test/ViewTest.php new file mode 100644 index 00000000..5139f733 --- /dev/null +++ b/test/ViewTest.php @@ -0,0 +1,293 @@ +request = new Request; + $this->response = new Response; + $this->model = new ViewModel; + $this->view = new View; + + $this->view->setRequest($this->request); + $this->view->setResponse($this->response); + } + + public function attachTestStrategies() + { + $this->view->addRenderingStrategy(function ($e) { + return new TestAsset\Renderer\VarExportRenderer(); + }); + $this->result = $result = new stdClass; + $this->view->addResponseStrategy(function ($e) use ($result) { + $result->content = $e->getResult(); + }); + } + + public function testRendersViewModelWithNoChildren() + { + $this->attachTestStrategies(); + $variables = array( + 'foo' => 'bar', + 'bar' => 'baz', + ); + $this->model->setVariables($variables); + $this->view->render($this->model); + + foreach ($variables as $key => $value) { + $expect = sprintf("'%s' => '%s',", $key, $value); + $this->assertContains($expect, $this->result->content); + } + } + + public function testRendersViewModelWithChildren() + { + $this->attachTestStrategies(); + + $child1 = new ViewModel(array('foo' => 'bar')); + + $child2 = new ViewModel(array('bar' => 'baz')); + + $this->model->setVariable('parent', 'node'); + $this->model->addChild($child1, 'child1'); + $this->model->addChild($child2, 'child2'); + + $this->view->render($this->model); + + $expected = var_export(new ViewVariables(array( + 'parent' => 'node', + 'child1' => var_export(array('foo' => 'bar'), true), + 'child2' => var_export(array('bar' => 'baz'), true), + )), true); + $this->assertEquals($expected, $this->result->content); + } + + public function testRendersTreeOfModels() + { + $this->attachTestStrategies(); + + $child1 = new ViewModel(array('foo' => 'bar')); + $child1->setCaptureTo('child1'); + + $child2 = new ViewModel(array('bar' => 'baz')); + $child2->setCaptureTo('child2'); + $child1->addChild($child2); + + $this->model->setVariable('parent', 'node'); + $this->model->addChild($child1); + + $this->view->render($this->model); + + $expected = var_export(new ViewVariables(array( + 'parent' => 'node', + 'child1' => var_export(array( + 'foo' => 'bar', + 'child2' => var_export(array('bar' => 'baz'), true), + ), true), + )), true); + $this->assertEquals($expected, $this->result->content); + } + + public function testChildrenMayInvokeDifferentRenderingStrategiesThanParents() + { + $this->view->addRenderingStrategy(function ($e) { + $model = $e->getModel(); + if (!$model instanceof ViewModel) { + return; + } + return new TestAsset\Renderer\VarExportRenderer(); + }); + $this->view->addRenderingStrategy(function ($e) { + $model = $e->getModel(); + if (!$model instanceof JsonModel) { + return; + } + return new Renderer\JsonRenderer(); + }, 10); // higher priority, so it matches earlier + $this->result = $result = new stdClass; + $this->view->addResponseStrategy(function ($e) use ($result) { + $result->content = $e->getResult(); + }); + + $child1 = new ViewModel(array('foo' => 'bar')); + $child1->setCaptureTo('child1'); + + $child2 = new JsonModel(array('bar' => 'baz')); + $child2->setCaptureTo('child2'); + $child2->setTerminal(false); + + $this->model->setVariable('parent', 'node'); + $this->model->addChild($child1); + $this->model->addChild($child2); + + $this->view->render($this->model); + + $expected = var_export(new ViewVariables(array( + 'parent' => 'node', + 'child1' => var_export(array('foo' => 'bar'), true), + 'child2' => json_encode(array('bar' => 'baz')), + )), true); + $this->assertEquals($expected, $this->result->content); + } + + public function testTerminalChildRaisesException() + { + $this->attachTestStrategies(); + + $child1 = new ViewModel(array('foo' => 'bar')); + $child1->setCaptureTo('child1'); + $child1->setTerminal(true); + + $this->model->setVariable('parent', 'node'); + $this->model->addChild($child1); + + $this->setExpectedException('Zend\View\Exception\DomainException'); + $this->view->render($this->model); + } + + public function testChildrenAreCapturedToParentVariables() + { + // I wish there were a "markTestRedundant()" method in PHPUnit + $this->testRendersViewModelWithChildren(); + } + + public function testOmittingCaptureToValueInChildLeadsToOmissionInParent() + { + $this->attachTestStrategies(); + + $child1 = new ViewModel(array('foo' => 'bar')); + $child1->setCaptureTo('child1'); + + // Deliberately disable the "capture to" declaration + $child2 = new ViewModel(array('bar' => 'baz')); + $child2->setCaptureTo(null); + + $this->model->setVariable('parent', 'node'); + $this->model->addChild($child1); + $this->model->addChild($child2); + + $this->view->render($this->model); + + $expected = var_export(new ViewVariables(array( + 'parent' => 'node', + 'child1' => var_export(array('foo' => 'bar'), true), + )), true); + $this->assertEquals($expected, $this->result->content); + } + + public function testResponseStrategyIsTriggeredForParentModel() + { + // I wish there were a "markTestRedundant()" method in PHPUnit + $this->testRendersViewModelWithChildren(); + } + + public function testResponseStrategyIsNotTriggeredForChildModel() + { + $this->view->addRenderingStrategy(function ($e) { + return new Renderer\JsonRenderer(); + }); + + $result = new ArrayObject; + $this->view->addResponseStrategy(function ($e) use ($result) { + $result[] = $e->getResult(); + }); + + $child1 = new ViewModel(array('foo' => 'bar')); + $child1->setCaptureTo('child1'); + + $child2 = new ViewModel(array('bar' => 'baz')); + $child2->setCaptureTo('child2'); + + $this->model->setVariable('parent', 'node'); + $this->model->addChild($child1); + $this->model->addChild($child2); + + $this->view->render($this->model); + + $this->assertEquals(1, count($result)); + } + + public function testUsesTreeRendererInterfaceToDetermineWhetherOrNotToPassOnlyRootViewModelToPhpRenderer() + { + $resolver = new Resolver\TemplateMapResolver(array( + 'layout' => __DIR__ . '/_templates/nested-view-model-layout.phtml', + 'content' => __DIR__ . '/_templates/nested-view-model-content.phtml', + )); + $phpRenderer = new PhpRenderer(); + $phpRenderer->setCanRenderTrees(true); + $phpRenderer->setResolver($resolver); + + $this->view->addRenderingStrategy(function ($e) use ($phpRenderer) { + return $phpRenderer; + }); + + $result = new stdClass; + $this->view->addResponseStrategy(function ($e) use ($result) { + $result->content = $e->getResult(); + }); + + $layout = new ViewModel(); + $layout->setTemplate('layout'); + $content = new ViewModel(); + $content->setTemplate('content'); + $content->setCaptureTo('content'); + $layout->addChild($content); + + $this->view->render($layout); + + $this->assertContains('Layout start', $result->content); + $this->assertContains('Content for layout', $result->content, $result->content); + $this->assertContains('Layout end', $result->content); + } + + public function testUsesTreeRendererInterfaceToDetermineWhetherOrNotToPassOnlyRootViewModelToJsonRenderer() + { + $jsonRenderer = new Renderer\JsonRenderer(); + + $this->view->addRenderingStrategy(function ($e) use ($jsonRenderer) { + return $jsonRenderer; + }); + + $result = new stdClass; + $this->view->addResponseStrategy(function ($e) use ($result) { + $result->content = $e->getResult(); + }); + + $layout = new ViewModel(array('status' => 200)); + $content = new ViewModel(array('foo' => 'bar')); + $content->setCaptureTo('response'); + $layout->addChild($content); + + $this->view->render($layout); + + $expected = json_encode(array( + 'status' => 200, + 'response' => array('foo' => 'bar'), + )); + + $this->assertEquals($expected, $result->content); + } +} diff --git a/test/_stubs/FilterDir1/Foo.php b/test/_stubs/FilterDir1/Foo.php new file mode 100644 index 00000000..265e33b8 --- /dev/null +++ b/test/_stubs/FilterDir1/Foo.php @@ -0,0 +1,24 @@ +view = $view; + return $this; + } +} diff --git a/test/_stubs/scripts/LfiProtectionCheck.phtml b/test/_stubs/scripts/LfiProtectionCheck.phtml new file mode 100644 index 00000000..7226a50b --- /dev/null +++ b/test/_stubs/scripts/LfiProtectionCheck.phtml @@ -0,0 +1,3 @@ +

      + Sample content for LFI protection check +

      diff --git a/test/_templates/block.phtml b/test/_templates/block.phtml new file mode 100644 index 00000000..8aec0cd0 --- /dev/null +++ b/test/_templates/block.phtml @@ -0,0 +1,6 @@ +addTemplate('layout'); +$this->placeholder('block')->captureStart(); +?> +Block content +placeholder('block')->captureEnd(); ?> diff --git a/test/_templates/empty.phtml b/test/_templates/empty.phtml new file mode 100644 index 00000000..6e1b56a5 --- /dev/null +++ b/test/_templates/empty.phtml @@ -0,0 +1 @@ +Empty view diff --git a/test/_templates/layout.phtml b/test/_templates/layout.phtml new file mode 100644 index 00000000..3a137190 --- /dev/null +++ b/test/_templates/layout.phtml @@ -0,0 +1,5 @@ + + +placeholder('block') ?> + + diff --git a/test/_templates/nested-view-model-child2.phtml b/test/_templates/nested-view-model-child2.phtml new file mode 100644 index 00000000..d800c35c --- /dev/null +++ b/test/_templates/nested-view-model-child2.phtml @@ -0,0 +1,2 @@ +Second child + diff --git a/test/_templates/nested-view-model-complexlayout.phtml b/test/_templates/nested-view-model-complexlayout.phtml new file mode 100644 index 00000000..35c46851 --- /dev/null +++ b/test/_templates/nested-view-model-complexlayout.phtml @@ -0,0 +1,5 @@ +Content: +renderChildModel('content') ?> + +Sidebar: +renderChildModel('sidebar') ?> diff --git a/test/_templates/nested-view-model-content.phtml b/test/_templates/nested-view-model-content.phtml new file mode 100644 index 00000000..a54c478b --- /dev/null +++ b/test/_templates/nested-view-model-content.phtml @@ -0,0 +1 @@ +Content for layout diff --git a/test/_templates/nested-view-model-layout.phtml b/test/_templates/nested-view-model-layout.phtml new file mode 100644 index 00000000..a5319ee7 --- /dev/null +++ b/test/_templates/nested-view-model-layout.phtml @@ -0,0 +1,6 @@ +Layout start + +renderChildModel('content'); ?> + + +Layout end diff --git a/test/_templates/test-with-helpers.phtml b/test/_templates/test-with-helpers.phtml new file mode 100644 index 00000000..df89fbb9 --- /dev/null +++ b/test/_templates/test-with-helpers.phtml @@ -0,0 +1,5 @@ +htmlList(array( + 'foo', + 'bar', + 'baz', +)); diff --git a/test/_templates/test.phtml b/test/_templates/test.phtml new file mode 100644 index 00000000..d3371388 --- /dev/null +++ b/test/_templates/test.phtml @@ -0,0 +1,3 @@ + +foo vars('bar') ?> baz diff --git a/test/_templates/testLocalVars.phtml b/test/_templates/testLocalVars.phtml new file mode 100644 index 00000000..6c67d933 --- /dev/null +++ b/test/_templates/testLocalVars.phtml @@ -0,0 +1 @@ + diff --git a/test/_templates/testNestedInner.phtml b/test/_templates/testNestedInner.phtml new file mode 100644 index 00000000..f05648e7 --- /dev/null +++ b/test/_templates/testNestedInner.phtml @@ -0,0 +1 @@ +inner diff --git a/test/_templates/testNestedOuter.phtml b/test/_templates/testNestedOuter.phtml new file mode 100644 index 00000000..dcd06490 --- /dev/null +++ b/test/_templates/testNestedOuter.phtml @@ -0,0 +1,4 @@ + +render('testNestedInner.phtml') ?> +content ?> diff --git a/test/_templates/testParent.phtml b/test/_templates/testParent.phtml new file mode 100644 index 00000000..7212e8e0 --- /dev/null +++ b/test/_templates/testParent.phtml @@ -0,0 +1,7 @@ +render($this->content); +$logger = new Log\Logger(new Log\Writer\Stream(__DIR__ . '/view.log')); +$logger->log(var_export($content, 1), Log\Logger::DEBUG); +$logger->log(var_export(__FILE__, 1), Log\Logger::DEBUG); diff --git a/test/_templates/testStrictVars.phtml b/test/_templates/testStrictVars.phtml new file mode 100644 index 00000000..b22eb6af --- /dev/null +++ b/test/_templates/testStrictVars.phtml @@ -0,0 +1,5 @@ + +Testing strict variables +foo ?> should emit a notice. +As should bar ?>. diff --git a/test/_templates/testSubTemplate.phtml b/test/_templates/testSubTemplate.phtml new file mode 100644 index 00000000..167ae714 --- /dev/null +++ b/test/_templates/testSubTemplate.phtml @@ -0,0 +1,3 @@ + +This text should not be displayed. diff --git a/test/_templates/testZf995.phtml b/test/_templates/testZf995.phtml new file mode 100644 index 00000000..2a5a9c40 --- /dev/null +++ b/test/_templates/testZf995.phtml @@ -0,0 +1,3 @@ +error +?> diff --git a/test/_templates/view-model-variables.phtml b/test/_templates/view-model-variables.phtml new file mode 100644 index 00000000..2659db44 --- /dev/null +++ b/test/_templates/view-model-variables.phtml @@ -0,0 +1 @@ +foo ?> diff --git a/test/_templates/view.phar b/test/_templates/view.phar new file mode 100644 index 0000000000000000000000000000000000000000..a333028d85ef5ffc01708a6c09c7df5e37326d0d GIT binary patch literal 7031 zcmb_hOLN=E5%wmBR9HzWd&qsn!Wv*CilY45P_isprYu(^i=nvMifI=L1coG5cm)9U zTJKivA?H+n!0vCzDYqQ*2Xf9ae41d5<{53x-X)6?D4-P6U*nI^TG zamh#QDKq`Z<5%k-nOn7*KW3I6*-n}|*Or+~ozz@oS)Otyv?n~bFH?)&Wbfaz#f9eBdMBp?vmpXKZ~i2<@^r@GyAk>! z&#=RCGi!)Jl9}x34%=L-nXY&;bAFY#lECpJlZe&SB&lRxU9BQ}xmQJ=RuO}EQbkUq zN^*XcS8-oul?zfrle2C3g)}IhoeN)Tx$r$6w`d5CQK)?MLubNUG^BWBGGH=HGy-wr zkEH_1N<&bmc_^tOq8J5=8Z04~HEVX?jmxU9nnZx|FA`p+V>ppy6(?!QEg93~x_O7L zmc$7TeAmhSIBH!rzi=*`%uW3yCzYY^sqi>6&yQPTEa_6^9dfKT=HN}5r!m@D2wH$gvvqjEj*d0C!X4ZvMWS{szfJ`be@KwW|tKb*)(>~ zFP#hC9HWj+i7%huTnvR$@m&h3rbD>w4Gp2>YNT>Onm|LMA|-?hks&l%tKpc|rrhy( zYSs50cgmalag?WV&|y*BbcwRY!daF#Q-0wDzUSn;4k0bmOPxvR+%nT#%@{cukDQR> z?CVEMS5+`jjbA}cPvWei8i{&_0;(lNjd7=*pIY;k#>jb?kUCk&MaeDKO(V%+KsFm; zspaeZa68y4HKKq=R>?Ny4Xs5*la@pgZJE@8qyf|kih!AigF&y=UT?Dp8*O$RUvQ80 zcp5qpkMiK!ybY~+6w4#hVbqis%dvHV@q>xk7DmyicH%(v_O*ysXCG;$=6uDB;Nvy zkXAxaLo3%yAtNk8;qs$#Om3>3RZVp?G|{Pel%Z&$lhXETC+Wms_nL$2gm+k}p_MjM z^BnPLl27H9j^SeF-^iF!;yNCYi@9g}7w9U6@BcK4opPw>T%x zt=HctxG5aw}SYfL=?~r)H%p1)gE z2qLp6aolsuJOeZqC*2zPk&|9G#*{-+=pg}9BLugeGzngNF%ki`I38v`ZU1IdX1Q@R zY`??#(e*8)GfvZ@j4Ad-CQ)k8xv0+SWlA=Pb)*^VPnkiCkYJZnI@^|Bi^u}LYJyl? zECyN8EGlN;hUat3?DqR7{SHF`^~W5BpgiM%QSI~CrIWEbQW5(h%QiHxn{|e^I}1EE z;&N8{g+f}Vz$3W~Dgkgw2HsZTHbQb`47!*f0>Qph_JMj|ald>cjv941z5T`YWO4(= z0Xisvb76m)#%YJ$1pbH>1%~+<+HcO#L`MNSM3bB_Ss`yVm{!gr&ONY<<5VbVx^bAK z92LW3xKlZ4CY@5(h?1qq`sJpMZal6vEm}Le@srK(HMgu|iN&Lv%?}tX4sx*%d6*Ce z&^fUpXVIZn{0!GZKc`q(5)_EE%@75gKC0Su``&10+H zU@It{>bRC?Nt-|wcZHU#T5YSuec}yEa_enrKF=E zs&b1M;jZjndbDA?;kodN7}wawlP6Esn7$+m14>{;ZlF3?^U;#FE&(o~_ei!UT}tKU zRGs0J<0`-hVRArCS7ymtVc~~O+!b4v%yJv0o;tmfFR<6*(kSB;ry5Qr2ZlzTP)Qk@ zq4|!XC>b&*cbGw{MY0lLoAVX{lYmQktzjOrnM%6~>4&)PMA{8FssWm!K=s8r61EI= zg(SloXawj6w>Y-EeTCCPPA&m+zG56*4MpwyVx`37wy1QYJ+ah;hzQ!+I1ygD(=bLy zTZ{o#e*E|`tt|f5#)jRCjC#Trf&tJU; zw!bY5a9Loe3n!H+&!Y>=931z+^^6j%H16DvyTm|-DOg}Yo_rx)ss@-X@)~MT7v%ZT zl#(3MOJaOIM@p1VQMt5IT43Hl;QvY!lyq4$nL&ycf>r8`1ZY&>EaMY>{R**RYfPXL zAOTpigsH8$1a>J&w;$sEWdy(}FvXzPN=q~p3aEtx;~B{Lln{zgkU;@xa8fjI$wKFp z&Zrony#xHn0}Ea}t*%nUu{e*wv1D1fjB-(}xr#-%AV@t&w;^>MMLe_w>D6utQz52k z5N1poi_$SH6ybErw`w*nBFb&N`Jft2DB5tzB{4rF(`yRR8m=fcbQg2W z_*sae$L^M~u}c9XMM%-2dVBhV7WoiK1X_dMF3mfRWCf$n*7_$iT>jnEeXwci`-rp~8Bt2rA7&!neYxk^ zER<&I#gRgIFVBQpVa?sko?VSK34sUXPf{Fcu^fi5d_s{2l`Rhk`z@MMvGn)BMC!xCP>0KIJ$bXkmUdVM&bBBrban z?zp&1`WIZhwZdH>%hzka_~yH}|Ni^OfBL%jkB#eph3Vh^{GV@r1Q%XDKYso{nBH2% literal 0 HcmV?d00001 diff --git a/test/bootstrap.php b/test/bootstrap.php new file mode 100644 index 00000000..ca213531 --- /dev/null +++ b/test/bootstrap.php @@ -0,0 +1,34 @@ +