diff --git a/doc/book/assets-listener.md b/doc/book/assets-listener.md new file mode 100644 index 000000000..1b3e6dce4 --- /dev/null +++ b/doc/book/assets-listener.md @@ -0,0 +1,38 @@ +# Assets listener + +Allow to use assets (css, js etc.) from modules, or other not public folders + +Web server has not access to module folder directly, but this is possible through `AssetsListener`. +`Zend\View\Helper\Assets` helper can build link to module folder (through Router), +`AssetsListener` can detect this link (through Router too) and route it to module name and asset. +When `AssetsListener` detected asset link, it caching module asset to public folder and +browser can request this asset directly from public folder (it can be disabled). +Also `AssetsListener` can filter assets content, as sample .less to .css. + +## Basic Usage + +Configure template resolver +```php +// Add temptate resolver for assets in config file +'assets_manager' => [ + 'template_resolver' => [ + 'prefix_resolver' => [ + 'ModuleName::' => __DIR__ . '\ModuleNameDir\assets' + ], + ], +], +``` + +Add asset in layout +```php +assets()->add('ModuleName::foo.css'); +echo $this->assets(); +?> +``` + +Layout Output: +```html + +``` \ No newline at end of file diff --git a/src/Application.php b/src/Application.php index e9ee14648..e4a31eda5 100644 --- a/src/Application.php +++ b/src/Application.php @@ -63,6 +63,7 @@ class Application implements */ protected $defaultListeners = [ 'RouteListener', + 'AssetsListener', 'MiddlewareListener', 'DispatchListener', 'HttpMethodListener', diff --git a/src/AssetsListener.php b/src/AssetsListener.php new file mode 100644 index 000000000..c492b05dc --- /dev/null +++ b/src/AssetsListener.php @@ -0,0 +1,367 @@ +listeners[] = $events->attach(MvcEvent::EVENT_DISPATCH, array($this, 'onDispatch'), 20); + $this->listeners[] = $events->attach(MvcEvent::EVENT_BOOTSTRAP, array($this, 'onBootstrap'), -1000); + } + + public function onBootstrap(MvcEvent $event) + { + if (!$this->useInternalRouter) { + return; + } + + if ($this->router) { + $router = $this->router; + } else { + $router = [ + 'type' => \Zend\Router\Http\Segment::class, + 'options' => [ + 'route' => '/' . $this->routerCacheFolder . '[/alias-:alias][/prefix-:prefix]/:asset', + 'constraints' => [ + 'alias' => '[a-zA-Z][.a-zA-Z0-9_-]*', + 'prefix' => '[a-zA-Z][a-zA-Z0-9_-]*', + 'asset' => '\S*', + ], + 'defaults' => [ + 'alias' => null, + 'prefix' => null, + ], + ], + ]; + } + + $event->getRouter()->addRoute( + end(explode('/', $this->routeName)), + $router + ); + } + + /** + * Listen to the "dispatch" event + * + * @param MvcEvent $event + * @return null|Response + */ + public function onDispatch(MvcEvent $event) + { + try { + $asset = $this->detectAsset($event); + + if (!$asset) { + return; + } + if ($asset instanceof ResponseInterface) { + return $asset; + } + + if (!($assetFile = $this->getAssetsResolver()->resolve($asset->getSource()))) { + return $event->getResponse() + ->setStatusCode(404) + ->setContent(sprintf( + 'can not resolve "%s" asset', + $asset->getName() + )); + } + + $target = $this->filter($event, $assetFile, $asset); + $result = $this->cache($event, $target); + $mime = $this->getAssetsManager()->getMimeResolver()->resolve($assetFile); + + return $this->complete($event, $result, $mime); + } catch (\Exception $ex) { + return $event->getResponse() + ->setStatusCode(500) + ->setContent($ex->getMessage()); + } + } + + /** + * @param MvcEvent $event + * @return null|array + */ + protected function detectAsset(MvcEvent $event) + { + $routeMatch = $event->getRouteMatch(); + if (!$routeMatch || $routeMatch->getMatchedRouteName() != $this->routeName) { + return; + } + + $aliasName = $routeMatch->getParam('alias'); + $assetName = Asset::normalizeName([ + $routeMatch->getParam('prefix'), + $routeMatch->getParam('asset')] + ); + + if (!$aliasName) { + return new Asset($assetName); + } + + $alias = $this->getAssetsManager()->get($aliasName); + if (!$alias) { + return $event->getResponse()->setStatusCode(404)->setContent(sprintf( + 'alias "%s" not found', + $aliasName + )); + } + $asset = $alias->get($assetName); + if (!$asset) { + return $event->getResponse()->setStatusCode(404)->setContent(sprintf( + 'asset "%s" not found in alias "%s"', + $assetName, + $aliasName + )); + } + return $asset; + } + + protected function complete(MvcEvent $event, $return = null, $mimetype = null) + { + if (is_string($return)) { + $response = $event->getResponse(); + $response->setContent($return); + $contentLength = function_exists('mb_strlen') ? mb_strlen($return, '8bit') : strlen($return); + } elseif (is_resource($return)) { + $response = new StreamResponse(); + $response->setStream($return); + $contentLength = fstat($return)['size']; + } else { + throw new Exception\InvalidArgumentException(sprintf( + '%s: expects "$return" parameter an null or string or resource, received "%s"', + __METHOD__, + (is_object($return) ? get_class($return) : gettype($return)) + )); + } + + $response->getHeaders()->clearHeaders()->addHeaders([ + 'Content-Transfer-Encoding' => 'binary', + 'Content-Type' => $mimetype, + 'Content-Length' => $contentLength, + ]); + return $response; + } + + protected function filter(MvcEvent $event, $assetFile, $assetItem) + { + $filters = $this->getAssetsManager()->getAssetFilters($assetItem); + if (!$filters) { + return fopen($assetFile, 'r'); + } + + $assetContent = file_get_contents($assetFile); + foreach($filters as $filter) { + if (!$filter instanceof FilterInterface) { + $filter = $this->getFilterManager()->get($filter); + } + $assetContent = $filter->filter($assetContent); + } + return $assetContent; + } + + protected function cache(MvcEvent $event, $source) + { + if (!$this->isCacheToPublic) { + return $source; + } + + $request = $event->getRequest(); + $targetFile = $this->getAssetsManager()->getPublicFolder() . substr($request->getRequestUri(), strlen($request->getBasePath())); + + $targetDir = dirname($targetFile); + if (!file_exists($targetDir) && @mkdir($targetDir, 0777, true) === false) { + throw new Exception\RuntimeException('can not create folder for caching asset'); + } + + if (is_string($source)) { + if (@file_put_contents($targetFile, $source) !== false) { + return $source; + } + throw new Exception\RuntimeException('can not save file to cache'); + } + + if (is_resource($source)) { + $target = fopen($targetFile, 'x+'); + if (@stream_copy_to_stream($source, $target)) { + fclose($source); + fseek($target, 0); + return $target; + } + throw new Exception\RuntimeException('can not save file to cache'); + } + + throw new Exception\InvalidArgumentException(sprintf( + '%s: expects "$source" parameter an string or resource, received "%s"', + __METHOD__, + (is_object($source) ? get_class($source) : gettype($source)) + )); + } + + public function setCacheToPublic($flag) + { + $this->isCacheToPublic = (bool)$flag; + return $this; + } + + public function isCacheToPublic() + { + return $this->isCacheToPublic; + } + + /** + * @param string $routeName + * @return self + */ + public function setRouteName($routeName) + { + if (!is_string($routeName)) { + throw new Exception\InvalidArgumentException(sprintf( + '%s: expects parameter an %s, received "%s"', + __METHOD__, + 'string', + (is_object($routeName) ? get_class($routeName) : gettype($routeName)) + )); + } + $this->routeName = $routeName; + return $this; + } + + /** + * @param array|RouteStackInterface $router + * @return self + */ + public function setRouter($router) + { + if (!(is_array($router) || $router instanceof RouteInterface)) { + throw new Exception\InvalidArgumentException(sprintf( + '%s: expects parameter an %s or %s, received "%s"', + __METHOD__, + 'array', + RouteInterface::class, + (is_object($router) ? get_class($router) : gettype($router)) + )); + } + $this->router = $router; + return $this; + } + + public function setRouterCacheFolder($routerCacheFolder) + { + $this->routerCacheFolder = trim($routerCacheFolder, '/'); + return $this; + } + + public function setUseInternalRouter($useInternalRouter) + { + $this->useInternalRouter = (bool)$useInternalRouter; + return $this; + } + + /** + * @return AssetsManager + */ + public function getAssetsManager() + { + return $this->assetsManager; + } + + /** + * @param AssetsManager $assetsManager + * @return self + */ + public function setAssetsManager(AssetsManager $assetsManager) + { + $this->assetsManager = $assetsManager; + return $this; + } + + /** + * @return ResolverInterface + */ + public function getAssetsResolver() + { + return $this->assetsResolver; + } + + /** + * @param ResolverInterface $resolver + * @return self + */ + public function setAssetsResolver(ResolverInterface $resolver) + { + $this->assetsResolver = $resolver; + return $this; + } + + /** + * @return PluginManagerInterface + */ + public function getFilterManager() + { + return $this->filterManager; + } + + /** + * @param PluginManagerInterface $filterManager + * @return self + */ + public function setFilterManager(PluginManagerInterface $filterManager) + { + $this->filterManager = $filterManager; + return $this; + } +} diff --git a/src/Service/AssetsListenerFactory.php b/src/Service/AssetsListenerFactory.php new file mode 100644 index 000000000..5a73a47a2 --- /dev/null +++ b/src/Service/AssetsListenerFactory.php @@ -0,0 +1,66 @@ +setAssetsManager($container->get('AssetsManager')); + $listener->setAssetsResolver($container->get('ViewAssetsResolver')); + $listener->setFilterManager($container->get('FilterManager')); + + $config = $container->get('config'); + + if (isset($config['assets_manager']['router_name'])) { + $listener->setRouteName($config['assets_manager']['router_name']); + } + if (isset($config['assets_manager']['router_cache_folder'])) { + $listener->setRouterCacheFolder($config['assets_manager']['router_cache_folder']); + } + if (isset($config['assets_manager']['use_internal_router'])) { + $listener->setUseInternalRouter($config['assets_manager']['use_internal_router']); + } + if (isset($config['assets_manager']['router'])) { + $listener->setRouter($config['assets_manager']['router']); + } + if (isset($config['assets_manager']['cache_to_public'])) { + $listener->setCacheToPublic($config['assets_manager']['cache_to_public']); + } + return $listener; + } + + /** + * Create and return DispatchListener instance + * + * For use with zend-servicemanager v2; proxies to __invoke(). + * + * @param ServiceLocatorInterface $container + * @return DispatchListener + */ + public function createService(ServiceLocatorInterface $container) + { + return $this($container, AssetsListener::class); + } +} diff --git a/src/Service/ServiceListenerFactory.php b/src/Service/ServiceListenerFactory.php index 758e62c40..a5e8aa43c 100644 --- a/src/Service/ServiceListenerFactory.php +++ b/src/Service/ServiceListenerFactory.php @@ -43,6 +43,7 @@ class ServiceListenerFactory implements FactoryInterface 'configuration' => 'config', 'Configuration' => 'config', 'HttpDefaultRenderingStrategy' => View\Http\DefaultRenderingStrategy::class, + 'AssetsListener' => 'Zend\Mvc\AssetsListener', 'MiddlewareListener' => 'Zend\Mvc\MiddlewareListener', 'RouteListener' => 'Zend\Mvc\RouteListener', 'SendResponseListener' => 'Zend\Mvc\SendResponseListener', @@ -80,11 +81,15 @@ class ServiceListenerFactory implements FactoryInterface 'ViewFeedStrategy' => 'Zend\Mvc\Service\ViewFeedStrategyFactory', 'ViewJsonStrategy' => 'Zend\Mvc\Service\ViewJsonStrategyFactory', 'ViewManager' => 'Zend\Mvc\Service\ViewManagerFactory', + 'AssetsManager' => 'Zend\View\Assets\Service\AssetsManagerFactory', + 'MimeResolver' => 'Zend\View\Assets\Service\MimeResolverFactory', + 'ViewAssetsResolver' => 'Zend\View\Assets\Service\AssetsResolverFactory', 'ViewResolver' => 'Zend\Mvc\Service\ViewResolverFactory', 'ViewTemplateMapResolver' => 'Zend\Mvc\Service\ViewTemplateMapResolverFactory', 'ViewTemplatePathStack' => 'Zend\Mvc\Service\ViewTemplatePathStackFactory', 'ViewPrefixPathStackResolver' => 'Zend\Mvc\Service\ViewPrefixPathStackResolverFactory', 'Zend\Mvc\MiddlewareListener' => InvokableFactory::class, + 'Zend\Mvc\AssetsListener' => 'Zend\Mvc\Service\AssetsListenerFactory', 'Zend\Mvc\RouteListener' => InvokableFactory::class, 'Zend\Mvc\SendResponseListener' => InvokableFactory::class, 'Zend\View\Renderer\FeedRenderer' => InvokableFactory::class, diff --git a/test/AssetsListenerTest.php b/test/AssetsListenerTest.php new file mode 100644 index 000000000..d852127be --- /dev/null +++ b/test/AssetsListenerTest.php @@ -0,0 +1,469 @@ +removeDirectory(); + } + + public function tearDown() + { + $this->removeDirectory(); + } + + /** + * + * @param string $asset + * @param array $assetsManagerConfig + * @return AssetsListener + */ + protected function prepareEnvironment($asset, $assetsManagerConfig = []) + { + if (!$this->serviceManager) { + $this->serviceManager = new ServiceManager([ + 'services' => [ + 'config' => ['assets_manager' => ArrayUtils::merge([ + 'public_folder' => $this->publicFolder, + 'template_resolver' => [ + 'map_resolver' => [ + 'style1.css' => __DIR__ . '\TestAsset\assets\style1.css', + 'style2.less' => __DIR__ . '\TestAsset\assets\style2.less', + ], + 'prefix_resolver' => [ + 'foo::' => __DIR__ . '\TestAsset\assets' + ], + 'path_resolver' => [], + ], + 'router_name' => $this->routeName, + 'router_cache_folder' => $this->routeUriPrefix, + 'use_internal_router' => true, + ], $assetsManagerConfig)], + 'FilterManager' => new FilterPluginManager(new ServiceManager([])), + 'Request' => new Request(), + ], + 'factories' => [ + 'ViewAssetsResolver' => 'Zend\View\Assets\Service\AssetsResolverFactory', + 'AssetsManager' => 'Zend\View\Assets\Service\AssetsManagerFactory', + 'AssetsListener' => 'Zend\Mvc\Service\AssetsListenerFactory', + 'MimeResolver' => 'Zend\View\Assets\Service\MimeResolverFactory', + 'RoutePluginManager' => 'Zend\Router\RoutePluginManagerFactory', + 'Router' => 'Zend\Router\RouterFactory', + 'HttpRouter' => 'Zend\Router\Http\HttpRouterFactory', + 'Assets' => 'Zend\View\Helper\Service\AssetsFactory', + ], + ]); + } + $this->assetsListener = $this->serviceManager->get('AssetsListener'); + $this->mvcEvent = $this->prepareAssetToMvcEvent($asset); + return $this->assetsListener; + } + + protected function prepareAssetToMvcEvent($asset) + { + $request = $this->serviceManager->get('Request'); + $router = $this->serviceManager->get('Router'); + + $mvcEvent = (new MvcEvent()) + ->setResponse(new ContentResponse()) + ->setRequest($request) + ->setRouter($router); + + if (!$asset) { + $asset = []; + } + if (is_array($asset)) { + $routeMatch = (new RouteMatch($asset))->setMatchedRouteName($this->routeName); + return $mvcEvent->setRouteMatch($routeMatch); + } + + $renderer = (new \Zend\View\Renderer\PhpRenderer()) + ->setHelperPluginManager(new \Zend\View\HelperPluginManager(new ServiceManager)); + + $renderer->plugin('url')->setRouter($router); + + $this->assetsListener->onBootstrap($mvcEvent); + + $mimeRenderer = $this->getMockBuilder(\stdClass::class) + ->setMethods(['itemToString']) + ->getMock(); + $mimeRenderer->method('itemToString')->will($this->returnCallback(function($params) { + return $params->href; + })); + + $href = trim($this->serviceManager->get('Assets') + ->setView($renderer) + ->add($asset) + ->setMimeRenderer('default', $mimeRenderer) + ->render(), "\n"); + + $request->setBasePath('')->setRequestUri($href)->getUri()->setPath($href); + + $routeMatch = $router->match($request); + if (!$routeMatch) { + $routeMatch = (new RouteMatch([]))->setMatchedRouteName($this->routeName); + } + $mvcEvent->setRouteMatch($routeMatch); + return $mvcEvent; + } + + public function testSetRouter() + { + $listener = new AssetsListener(new ServiceManager()); + + $listener->setRouter([]); + $listener->setRouter($this->getMock('Zend\Router\RouteInterface')); + + $this->setExpectedException( + Exception\InvalidArgumentException::class, + 'Zend\Mvc\AssetsListener::setRouter: expects parameter an array or Zend\Router\RouteInterface, received "stdClass"' + ); + $listener->setRouter(new \stdClass); + } + + public function testInjectCustomRouterViaConfig() + { + $routeName = 'asset_route_name'; + $this->prepareEnvironment('a1', [ + 'router_name' => $routeName, + 'router' => [ + 'type' => \Zend\Router\Http\Literal::class, + 'options' => [ + 'route' => '/foo', + ], + ], + ]); + $this->assertTrue($this->mvcEvent->getRouter()->hasRoute($routeName)); + $this->assertInstanceOf( + \Zend\Router\Http\Literal::class, + $this->mvcEvent->getRouter()->getRoute($routeName) + ); + } + + public function testInjectRouter() + { + $this->prepareEnvironment('a1', ['assets' => [ + 'default' => [ + 'a1' => ['assets' => 'foo::style1.css'], + ], + ]]); + $this->assertTrue($this->mvcEvent->getRouter()->hasRoute('foo')); + } + + public function testPrefixWithAliasToCache_StreamResponse() + { + $this->prepareEnvironment('a1', ['assets' => [ + 'default' => [ + 'a1' => ['assets' => 'foo::style1.css'], + ], + ]]); + + $response = $this->assetsListener->onDispatch($this->mvcEvent); + + $this->assertInstanceOf(StreamResponse::class, $response); + + $this->assertEquals([ + 'Content-Transfer-Encoding' => 'binary', + 'Content-Type' => 'text/css', + 'Content-Length' => 21, + ], $response->getHeaders()->toArray()); + + $streamFile = stream_get_meta_data($response->getStream())['uri']; + $this->assertStringEndsWith('/style1.css', $streamFile); + $this->assertStringStartsWith('./' . $this->publicFolder, $streamFile); + $this->assertEquals('.STYLE_1 { COLOR: 1;}', stream_get_contents($response->getStream())); + } + + public function testPrefixWithoutAliasToCache_StreamResponse() + { + $this->prepareEnvironment('foo::style1.css'); + $response = $this->assetsListener->onDispatch($this->mvcEvent); + + $this->assertInstanceOf(StreamResponse::class, $response); + + $this->assertEquals([ + 'Content-Transfer-Encoding' => 'binary', + 'Content-Type' => 'text/css', + 'Content-Length' => 21, + ], $response->getHeaders()->toArray()); + + $streamFile = stream_get_meta_data($response->getStream())['uri']; + $this->assertContains('foo/', $streamFile); + $this->assertContains($this->publicFolder, $streamFile); + $this->assertStringEndsWith('/style1.css', $streamFile); + + $this->assertEquals('.STYLE_1 { COLOR: 1;}', stream_get_contents($response->getStream())); + } + + public function testRenameCached() + { + $this->prepareEnvironment('a1', ['assets' => [ + 'default' => [ + 'a1' => ['assets' => [ + 'style2.css' => [ + 'source' => 'style2.less' + ] + ], + ], + ], + ]]); + $response = $this->assetsListener->onDispatch($this->mvcEvent); + + $this->assertInstanceOf(StreamResponse::class, $response); + + $this->assertEquals([ + 'Content-Transfer-Encoding' => 'binary', + 'Content-Type' => 'text/css', + 'Content-Length' => 21, + ], $response->getHeaders()->toArray()); + + $streamFile = stream_get_meta_data($response->getStream())['uri']; + $this->assertContains('a1/', $streamFile); + $this->assertContains($this->publicFolder, $streamFile); + $this->assertStringEndsWith('/style2.css', $streamFile); + + $this->assertEquals('.style2 { color: 22;}', stream_get_contents($response->getStream())); + } + + public function testRenameWithPrefixCached() + { + $this->prepareEnvironment('a1', ['assets' => [ + 'default' => [ + 'a1' => ['assets' => [ + 'foo::style2.css' => [ + 'source' => 'foo::style2.less' + ] + ], + ], + ], + ]]); + $response = $this->assetsListener->onDispatch($this->mvcEvent); + + $this->assertInstanceOf(StreamResponse::class, $response); + + $this->assertEquals([ + 'Content-Transfer-Encoding' => 'binary', + 'Content-Type' => 'text/css', + 'Content-Length' => 21, + ], $response->getHeaders()->toArray()); + + $streamFile = stream_get_meta_data($response->getStream())['uri']; + $this->assertContains('a1/', $streamFile); + $this->assertContains($this->publicFolder, $streamFile); + $this->assertStringEndsWith('/style2.css', $streamFile); + + $this->assertEquals('.style2 { color: 22;}', stream_get_contents($response->getStream())); + } + + public function testNoFilterStreamCached() + { + $this->prepareEnvironment('style1.css'); + $this->mvcEvent->getRouteMatch() + ->setParam('alias', null) + ->setParam('asset', 'style1.css'); + $response = $this->assetsListener->onDispatch($this->mvcEvent); + + $this->assertInstanceOf(StreamResponse::class, $response); + + $this->assertEquals([ + 'Content-Transfer-Encoding' => 'binary', + 'Content-Type' => 'text/css', + 'Content-Length' => 21, + ], $response->getHeaders()->toArray()); + + $streamFile = stream_get_meta_data($response->getStream())['uri']; + $this->assertContains($this->publicFolder, $streamFile); + $this->assertStringEndsWith('/style1.css', $streamFile); + + $this->assertEquals('.STYLE_1 { COLOR: 1;}', stream_get_contents($response->getStream())); + } + + public function testNoFilterStreamNotCached() + { + $this->prepareEnvironment('style1.css', [ + 'cache_to_public' => false, + ]); + $this->mvcEvent->getRouteMatch() + ->setParam('alias', null) + ->setParam('asset', 'style1.css'); + $response = $this->assetsListener->onDispatch($this->mvcEvent); + + $this->assertInstanceOf(StreamResponse::class, $response); + + $this->assertEquals([ + 'Content-Transfer-Encoding' => 'binary', + 'Content-Type' => 'text/css', + 'Content-Length' => 21, + ], $response->getHeaders()->toArray()); + + $this->assertFileNotExists($this->publicFolder . '/style1.css'); + $this->assertEquals(__DIR__ . '\TestAsset\assets\style1.css', stream_get_meta_data($response->getStream())['uri']); + $this->assertEquals('.STYLE_1 { COLOR: 1;}', stream_get_contents($response->getStream())); + } + + public function testFilterStringCached() + { + $this->prepareEnvironment('filtered', ['assets' => [ + 'default' => [ + 'filtered' => ['assets' => [ + 'style1.css' => [ + 'filters' => ['stringToLower'], + ], + ], + ], + ], + ]]); + + $response = $this->assetsListener->onDispatch($this->mvcEvent); + + $this->assertInstanceOf(ContentResponse::class, $response); + + $this->assertEquals([ + 'Content-Transfer-Encoding' => 'binary', + 'Content-Type' => 'text/css', + 'Content-Length' => 21, + ], $response->getHeaders()->toArray()); + + $this->assertFileExists($this->publicFolder); + $this->assertFileExists($this->publicFolder . '/assets/alias-filtered/style1.css'); + $this->assertEquals(200, $response->getStatusCode()); + $this->assertEquals('.style_1 { color: 1;}', $response->getContent()); + $this->assertEquals('.style_1 { color: 1;}', file_get_contents($this->publicFolder . '/assets/alias-filtered/style1.css')); + } + + public function testFilterStringNotCached() + { + $this->prepareEnvironment('filtered', [ + 'cache_to_public' => false, + 'assets' => [ + 'default' => [ + 'filtered' => ['assets' => [ + 'style1.css' => [ + 'filters' => ['stringToLower'], + ], + ]], + ], + ], + ]); + + $response = $this->assetsListener->onDispatch($this->mvcEvent); + + $this->assertInstanceOf(ContentResponse::class, $response); + + $this->assertEquals([ + 'Content-Transfer-Encoding' => 'binary', + 'Content-Type' => 'text/css', + 'Content-Length' => 21, + ], $response->getHeaders()->toArray()); + + $this->assertFileNotExists($this->publicFolder); + $this->assertEquals('.style_1 { color: 1;}', $response->getContent()); + } + + public function test404() + { + $this->prepareEnvironment(null, ['assets' => [ + 'default' => [ + 'alias1' => ['assets' => 'style2.css'], + ], + ]]); + + $this->mvcEvent->getRouteMatch() + ->setParam('alias', 'notFound'); + $response = $this->assetsListener->onDispatch($this->mvcEvent); + $this->assertInstanceOf(ContentResponse::class, $response); + $this->assertEquals('alias "notFound" not found', $response->getContent()); + $this->assertEquals(404, $response->getStatusCode()); + $this->assertEmpty($response->getHeaders()); + + $this->mvcEvent->getRouteMatch() + ->setParam('alias', 'alias1') + ->setParam('asset', 'notFound.css'); + $response = $this->assetsListener->onDispatch($this->mvcEvent); + $this->assertInstanceOf(ContentResponse::class, $response); + $this->assertEquals('asset "notFound.css" not found in alias "alias1"', $response->getContent()); + $this->assertEquals(404, $response->getStatusCode()); + $this->assertEmpty($response->getHeaders()); + + $this->mvcEvent->getRouteMatch() + ->setParam('alias', null) + ->setParam('asset', 'style2.css'); + $response = $this->assetsListener->onDispatch($this->mvcEvent); + $this->assertInstanceOf(ContentResponse::class, $response); + $this->assertEquals('can not resolve "style2.css" asset', $response->getContent()); + $this->assertEquals(404, $response->getStatusCode()); + $this->assertEmpty($response->getHeaders()); + } + + public function test500() + { + $mvcEvent = $this->getMock(MvcEvent::class, ['getRouteMatch', 'getResponse']); + $mvcEvent->method('getRouteMatch')->will($this->returnCallback(function() { + throw new \Exception('Exception 500'); + })); + $mvcEvent->method('getResponse')->will($this->returnValue(new ContentResponse())); + + $response = (new AssetsListener())->onDispatch($mvcEvent); + + $this->assertInstanceOf(ContentResponse::class, $response); + $this->assertEquals('Exception 500', $response->getContent()); + $this->assertEquals(500, $response->getStatusCode()); + $this->assertEmpty($response->getHeaders()); + } + + protected function removeDirectory($path = null) + { + if ($path === null) { + $path = current(explode('/', $this->publicFolder)); + } + if (!file_exists($path)) { + return; + } + foreach (glob($path . '/*') as $file) { + is_dir($file) ? $this->removeDirectory($file) : unlink($file); + } + rmdir($path); + } +} diff --git a/test/TestAsset/assets/style1.css b/test/TestAsset/assets/style1.css new file mode 100644 index 000000000..ec38b37e3 --- /dev/null +++ b/test/TestAsset/assets/style1.css @@ -0,0 +1 @@ +.STYLE_1 { COLOR: 1;} \ No newline at end of file diff --git a/test/TestAsset/assets/style2.less b/test/TestAsset/assets/style2.less new file mode 100644 index 000000000..5fd667a88 --- /dev/null +++ b/test/TestAsset/assets/style2.less @@ -0,0 +1 @@ +.style2 { color: 22;} \ No newline at end of file