Skip to content

Commit

Permalink
Add support for URI mapped document roots
Browse files Browse the repository at this point in the history
  • Loading branch information
Jason Terando committed Jul 2, 2020
1 parent c321b39 commit 5fefe8d
Show file tree
Hide file tree
Showing 10 changed files with 447 additions and 21 deletions.
59 changes: 59 additions & 0 deletions docs/book/v2/static-resources.md
Expand Up @@ -308,6 +308,65 @@ return [
];
```

## Prefixed Mapped Document Roots

The `Mezzio\Swoole\StaticResourceHandler\FileLocationRepository` implements the `Mezzio\Swoole\StaticResourceHandler\FileLocationRepositoryInterface` to maintain an association of URI prefixes with file directories. One use case would be if you have a module that contains a template, and that template relies on assets like JavaScript files, CSS files, etc. Instead of copying those assets to a public directory configured in `document-root`, you can leave the files in the module, and access them using a defined URI prefix.

To accomplish this:

1. Define what your URI prefix will be (ex. `/my-module`)
2. Update your template/s attributes like `href` and `src` to use the prefix (ex. `<script src='/my-module/style.css'></script>`)
3. In the factory of your handler, or whatever is rendering the template, set up the linkage between the prefix and the directory where your assets are located.

### Prefixed Mapped Document Roots - Example

Assume you have a module, AwesomeModule, which has a handler called "HomeHandler", which renders the 'home' template. You designate the prefix, `/awesome-home` for rendering the assets. The structure of your module looks like this:

```
AwesomeModule
├── src
| ├── Handler
| | ├── HomeHandler.php
| | ├── HomeHandlerFactory.php
| ├── ConfigProvider.php
├── templates
│ ├── home
| | ├── home.html
| | ├── style.css
│ ├── layouts
```

In your `home.html` template, you can refer to the `style.css` file as follows:

```
<link href="/awesome-home/style.css" rel="stylesheet" type="text/css">
```

Finally, in the factory for rendering the Home page, you create assignment for `/awesome-home` to your modules's `templates/home` directory:

```php
use Psr\Container\ContainerInterface;
use Mezzio\Template\TemplateRendererInterface;
use Mezzio\Swoole\StaticResourceHandler\FileLocationRepositoryInterface;

class AwesomeHomeHandlerFactory
{
public function __invoke(ContainerInterface $container) : DocumentationViewHandler
{
// Establish location for the home template assets
$repo = $container->get(FileLocationRepositoryInterface::class);
$repo->addMappedDocumentRoot('awesome-home',
realpath(__DIR__ . '/../../templates/home'));

return new AwesomeHomeHandler(
$container->get(TemplateRendererInterface::class)
);
}
}
```

When the template renders, the client will request `/awesome-home/style.css`, which the StaticResourceHandler will now retrieve from the `templates/home` folder of the module.

## Writing Middleware

Static resource middleware must implement
Expand Down
11 changes: 8 additions & 3 deletions src/ConfigProvider.php
Expand Up @@ -17,6 +17,9 @@
use Mezzio\Swoole\HotCodeReload\ReloaderFactory;
use Psr\Http\Message\ServerRequestInterface;
use Swoole\Http\Server as SwooleHttpServer;
use StaticResourceHandler\{
FileLocationRepository, FileLocationRepositoryFactory, FileLocationRepositoryInterface
};

use function extension_loaded;

Expand Down Expand Up @@ -76,14 +79,16 @@ public function getDependencies() : array
StaticResourceHandler::class => StaticResourceHandlerFactory::class,
SwooleHttpServer::class => HttpServerFactory::class,
Reloader::class => ReloaderFactory::class,
FileLocationRepository::class => FileLocationRepositoryFactory::class,
],
'invokables' => [
InotifyFileWatcher::class => InotifyFileWatcher::class,
],
'aliases' => [
RequestHandlerRunner::class => SwooleRequestHandlerRunner::class,
StaticResourceHandlerInterface::class => StaticResourceHandler::class,
FileWatcherInterface::class => InotifyFileWatcher::class,
RequestHandlerRunner::class => SwooleRequestHandlerRunner::class,
StaticResourceHandlerInterface::class => StaticResourceHandler::class,
FileWatcherInterface::class => InotifyFileWatcher::class,
FileLocationRepositoryInterface::class => FileLocationRepository::class,

// Legacy Zend Framework aliases
\Zend\Expressive\Swoole\Command\ReloadCommand::class => Command\ReloadCommand::class,
Expand Down
5 changes: 0 additions & 5 deletions src/StaticResourceHandler/ContentTypeFilterMiddleware.php
Expand Up @@ -12,7 +12,6 @@

use Swoole\Http\Request;

use function file_exists;
use function pathinfo;

use const PATHINFO_EXTENSION;
Expand Down Expand Up @@ -148,10 +147,6 @@ private function cacheFile(string $fileName) : bool
return false;
}

if (! file_exists($fileName)) {
return false;
}

$this->cacheTypeFile[$fileName] = $this->typeMap[$type];
return true;
}
Expand Down
108 changes: 108 additions & 0 deletions src/StaticResourceHandler/FileLocationRepository.php
@@ -0,0 +1,108 @@
<?php

/**
* @see https://github.com/mezzio/mezzio-swoole for the canonical source repository
* @copyright https://github.com/mezzio/mezzio-swoole/blob/master/COPYRIGHT.md
* @license https://github.com/mezzio/mezzio-swoole/blob/master/LICENSE.md New BSD License
*/

declare(strict_types=1);

namespace Mezzio\Swoole\StaticResourceHandler;

use InvalidArgumentException;

class FileLocationRepository implements FileLocationRepositoryInterface {
/**
* @var array
* Associative array of URI prefixes and directories
*/
private $mappedDocRoots = [];

/**
* Initialize repository with default mapped document roots
*/
public function __construct(array $defaultMappedDocRoots)
{
foreach($defaultMappedDocRoots as $prefix => $directories) {
foreach($directories as $directory) {
$this->addMappedDocumentRoot($prefix, $directory);
}
}
}

/**
* Add the specified directory to list of mapped directories
*/
public function addMappedDocumentRoot(string $prefix, string $directory): void
{
$valPrefix =$this->validatePrefix($prefix);
$valDirectory = $this->validateDirectory($directory, $valPrefix);

if(array_key_exists($valPrefix, $this->mappedDocRoots)) {
$dirs = &$this->mappedDocRoots[$valPrefix];
if(! in_array($valDirectory, $dirs)) {
$dirs[] = $valDirectory;
}
} else {
$this->mappedDocRoots[$valPrefix] = [$valDirectory];
}
}

/**
* Validate prefix, ensuring it is non-empty and starts and ends with a slash
*/
private function validatePrefix(string $prefix): string
{
if(empty($prefix)) {
// For the default prefix, set it to a slash to get matching to work
$prefix = '/';
} else {
if($prefix[0] != '/') $prefix = "/$prefix";
if($prefix[-1] != '/') $prefix .= '/';
}
return $prefix;
}

/**
* Validate directory, ensuring it exists and
*/
private function validateDirectory(string $directory, string $prefix): string
{
if(! is_dir($directory)) {
throw new InvalidArgumentException(sprintf(
'The document root for "%s", "%s", does not exist; please check your configuration.',
empty($prefix) ? "(Default)" : $prefix, $directory
));
}
if($directory[-1] != '/') $directory .= '/';
return $directory;
}

/**
* Return the mapped document roots
*/
public function listMappedDocumentRoots(): array
{
return $this->mappedDocRoots;
}

/**
* Searches for the specified file in mapped document root
* directories; returns the location if found, or null if not
*/
public function findFile(string $filename): ?string
{
foreach($this->mappedDocRoots as $prefix => $directories) {
foreach($directories as $directory) {
if(stripos($filename, $prefix) == 0) {
$mappedFileName = $directory . substr($filename, strlen($prefix));
if(file_exists($mappedFileName)) {
return $mappedFileName;
}
}
}
}
return null;
}
}
32 changes: 32 additions & 0 deletions src/StaticResourceHandler/FileLocationRepositoryFactory.php
@@ -0,0 +1,32 @@
<?php

/**
* @see https://github.com/mezzio/mezzio-swoole for the canonical source repository
* @copyright https://github.com/mezzio/mezzio-swoole/blob/master/COPYRIGHT.md
* @license https://github.com/mezzio/mezzio-swoole/blob/master/LICENSE.md New BSD License
*/

declare(strict_types=1);

namespace Mezzio\Swoole\StaticResourceHandler;

use Psr\Container\ContainerInterface;
use InvalidArgumentException;
use function getcwd;

class FileLocationRepositoryFactory
{
/**
* Create a file location repository, initializing with the static files setting configured by mezzio-swoole
*/
public function __invoke(ContainerInterface $container) : FileLocationRepository
{
$docRoots = $container->get('config')['mezzio-swoole']['swoole-http-server']['static-files']['document-root']
?? [getcwd() . '/public'];
if(! is_array($docRoots)) {
// Accomodate if the user defines document-root as a string or array
$docRoots = [$docRoots];
}
return new FileLocationRepository(count($docRoots) > 0 ? ['' => $docRoots] : []);
}
}
22 changes: 22 additions & 0 deletions src/StaticResourceHandler/FileLocationRepositoryInterface.php
@@ -0,0 +1,22 @@
<?php

/**
* @see https://github.com/mezzio/mezzio-swoole for the canonical source repository
* @copyright https://github.com/mezzio/mezzio-swoole/blob/master/COPYRIGHT.md
* @license https://github.com/mezzio/mezzio-swoole/blob/master/LICENSE.md New BSD License
*/

declare(strict_types=1);

namespace Mezzio\Swoole\StaticResourceHandler;

/**
* Interface to implement a repository for storing the association
* between the start of a URI (prefix) and directory
*/
interface FileLocationRepositoryInterface
{
function addMappedDocumentRoot(string $prefix, string $directory): void;
function listMappedDocumentRoots(): array;
function findFile(string $filename): ?string;
}
13 changes: 0 additions & 13 deletions test/StaticResourceHandler/ContentTypeFilterMiddlewareTest.php
Expand Up @@ -44,19 +44,6 @@ public function testCanProvideAlternateTypeMapViaConstructor()
$this->assertAttributeSame($typeMap, 'typeMap', $middleware);
}

public function testMiddlewareReturnsFailureResponseIfFileNotFound()
{
$next = static function ($request, $filename) {
TestCase::fail('Should not have invoked next middleware');
};
$middleware = new ContentTypeFilterMiddleware();

$response = $middleware($this->request, __DIR__ . '/not-a-valid-file.png', $next);

$this->assertInstanceOf(StaticResourceResponse::class, $response);
$this->assertTrue($response->isFailure());
}

public function testMiddlewareReturnsFailureResponseIfFileNotAllowedByTypeMap()
{
$next = static function ($request, $filename) {
Expand Down
75 changes: 75 additions & 0 deletions test/StaticResourceHandler/FileLocationRepositoryFactoryTest.php
@@ -0,0 +1,75 @@
<?php
/**
* @see https://github.com/mezzio/mezzio-swoole for the canonical source repository
* @copyright https://github.com/mezzio/mezzio-swoole/blob/master/COPYRIGHT.md
* @license https://github.com/mezzio/mezzio-swoole/blob/master/LICENSE.md New BSD License
*/

declare(strict_types=1);

namespace MezzioTest\Swoole;

require_once('_MockIsDir.php');

use Mezzio\Swoole\StaticResourceHandler\FileLocationRepository;
use Mezzio\Swoole\StaticResourceHandler\FileLocationRepositoryFactory;
use PHPUnit\Framework\TestCase;
use Psr\Container\ContainerInterface;

class FileLocationRepositoryFactoryTest extends TestCase
{
protected function setUp() : void
{
$this->container = $this->prophesize(ContainerInterface::class);
$this->fileLocRepoFactory = new FileLocationRepositoryFactory();
}

public function testFactoryReturnsFileLocationRepository()
{
$factory = $this->fileLocRepoFactory;
$fileLocRepo = $factory($this->container->reveal());
$this->assertInstanceOf(FileLocationRepository::class, $fileLocRepo);
}

public function testFactoryUsesConfiguredDocumentRoot()
{
$dir = getcwd() . '/public/';
$this->container->get('config')->willReturn([
'mezzio-swoole' => [
'swoole-http-server' => [
'static-files' => [
'document-root' => [$dir]
]
]
]
]);
$factory = $this->fileLocRepoFactory;
$fileLocRepo = $factory($this->container->reveal());
$this->assertEquals(['/' => [$dir]], $fileLocRepo->listMappedDocumentRoots());
}

public function testFactoryHasNoDefaultsIfEmptyDocumentRoot()
{
$dir = getcwd() . '/public/';
$this->container->get('config')->willReturn([
'mezzio-swoole' => [
'swoole-http-server' => [
'static-files' => [
'document-root' => []
]
]
]
]);
$factory = $this->fileLocRepoFactory;
$fileLocRepo = $factory($this->container->reveal());
$this->assertEquals([], $fileLocRepo->listMappedDocumentRoots());
}

public function testFactoryUsesDefaultDocumentRoot()
{
$dir = getcwd() . '/public/';
$factory = $this->fileLocRepoFactory;
$fileLocRepo = $factory($this->container->reveal());
$this->assertEquals(['/' => [$dir]], $fileLocRepo->listMappedDocumentRoots());
}
}

0 comments on commit 5fefe8d

Please sign in to comment.