Initial commit IQParts\Content

This commit is contained in:
2018-03-01 18:41:55 +01:00
commit 349fa9c003
23 changed files with 1535 additions and 0 deletions

8
.gitignore vendored Normal file
View File

@@ -0,0 +1,8 @@
.DS_Store
.idea/
composer.lock
vendor/
tmp/
.vs/
.vscode/
coverage/

10
.scrutinizer.yml Normal file
View File

@@ -0,0 +1,10 @@
filter:
paths: ["src/*"]
tools:
external_code_coverage: true
php_code_coverage: true
php_sim: true
php_mess_detector: true
php_pdepend: true
php_analyzer: true
php_cpd: true

19
LICENSE Normal file
View File

@@ -0,0 +1,19 @@
Copyright (c) 2018 Industrial Quality Parts
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is furnished
to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.

42
Readme.md Normal file
View File

@@ -0,0 +1,42 @@
# IQParts/Content
### Installation
`composer require iqparts/content`
### Content
This library is a small wrapper around [League\Flysystem](https://github.com/thephpleague/flysystem).
This library lets you do anything that Flysystem already can, but uses its own objects with some extra
functions.
### Resolver
This library also includes an AssetResolver which returns a PSR-7 response if the file exists
on the filesystem.
### Usage
``` php
// Filesystem
$fs = new IQParts\Content\Filesystem(
new League\Flysystem\AdapterInterface(),
new League\Flysystem\Config()
);
// AssetResolver
$resolver = new AssetResolver($fs);
if ($resolver->exists('my-file.txt') {
// PSR-7 $response
$response = $resolver->resolve(
$request, // PSR-7 request
$response, // PSR-7 response
$cacheControl // CacheControl object,
$inline // Inline or as attachment
);
}
// MimeDetector
$mimetype = (new MimeDetector([
'my-mime' => 'my-mimetype'
]))->detectFromExtension('document.pdf');
$mimetype = 'application/pdf'
```

30
composer.json Normal file
View File

@@ -0,0 +1,30 @@
{
"name": "iqparts/content",
"description": "Small wrapper around League/Flysystem with an added AssetResolver for resolving files as responses.",
"type": "library",
"license": "MIT",
"authors": [
{
"name": "Mèir Noordermeer",
"email": "meirnoordermeer@me.com"
}
],
"require": {
"league/flysystem": "^1.0",
"psr/http-message": "^1.0",
"guzzlehttp/psr7": "^1.4"
},
"require-dev": {
"phpunit/phpunit": "^7.0"
},
"autoload": {
"psr-4": {
"IQParts\\Content\\": ["src"]
}
},
"autoload-dev": {
"psr-4": {
"IQParts\\ContentTest\\": ["test"]
}
}
}

18
phpunit.xml Normal file
View File

@@ -0,0 +1,18 @@
<?xml version="1.0"?>
<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="vendor/phpunit/phpunit/phpunit.xsd"
colors="true"
verbose="true"
backupGlobals="false"
beStrictAboutOutputDuringTests="true"
beStrictAboutTestsThatDoNotTestAnything="true"
>
<testsuite name="IQParts Content tests">
<directory>./test/Unit</directory>
</testsuite>
<filter>
<whitelist addUncoveredFilesFromWhitelist="true">
<directory suffix=".php">./src</directory>
</whitelist>
</filter>
</phpunit>

86
src/AssetResolver.php Normal file
View File

@@ -0,0 +1,86 @@
<?php
namespace IQParts\Content;
use GuzzleHttp\Psr7\Stream;
use IQParts\Content\Object\CacheControl;
use IQParts\Content\Object\File;
use League\Flysystem\FileNotFoundException;
use Psr\Http\Message\RequestInterface;
use Psr\Http\Message\ResponseInterface;
/**
* Class AssetResolver
* @package IQParts\Framework\Content
*/
final class AssetResolver
{
/**
* @var MimeDetector
*/
private $detector;
/**
* @var Filesystem
*/
private $filesystem;
/**
* AssetResolver constructor.
* @param Filesystem $filesystem
* @internal param string $assetDir
*/
public function __construct(Filesystem $filesystem )
{
$this->detector = new MimeDetector();
$this->filesystem = $filesystem ;
}
/**
* @param string $file
* @return bool
*/
public function exists(string $file): bool
{
// Will never throw as we check if the file exists.
return $this->filesystem->exists($file) && $this->filesystem->get($file) instanceof File;
}
/**
* @param RequestInterface $request
* @param ResponseInterface $response
* @param CacheControl $cacheControl
* @param bool $inline
* @return ResponseInterface|bool
* @throws FileNotFoundException
*/
public function resolve(
RequestInterface $request,
ResponseInterface $response,
CacheControl $cacheControl,
bool $inline = true
)
{
$target = $request->getRequestTarget();
$file = $this->filesystem->get($target);
/** @var ResponseInterface $response */
$response = $response->withHeader('Cache-Control', (string) $cacheControl);
$response = $response->withHeader('Content-Type', $this->detector->detectFromExtension(
$file->getBasename())
);
if ($inline) {
$response = $response->withHeader('Content-Disposition', 'inline; filename="' . $file->getBasename() . '";');
} else {
$response = $response->withHeader('Content-Disposition', 'attachment; filename="' . $file->getBasename() . '";');
}
$response = $response->withHeader('Content-Length', (string)$file->getSize());
/**
* Will always return a stream
*/
$stream = $file->getStream();
return $response->withBody(new Stream($stream))->withStatus(200);
}
}

View File

@@ -0,0 +1,56 @@
<?php
namespace IQParts\Framework\Content;
interface ContentProviderInterface
{
/**
* @param string $location
* @return bool
*/
public function exists(string $location): bool;
/**
* @param string $location
* @param $data
* @return mixed
*/
public function put(string $location, $data);
/**
* @param string $location
* @return resource
*/
public function get(string $location);
/**
* @param string $location
* @return array
*/
public function getCollection(string $location): array;
/**
* @param string $location
* @return int
*/
public function getSize(string $location): int;
/**
* @param string $location
* @return bool
*/
public function remove(string $location): bool;
/**
* @param string $location
* @return string
*/
public function getPermissions(string $location): string;
/**
* @param string $location
* @return bool
*/
public function createCollection(string $location): bool;
}

197
src/Filesystem.php Normal file
View File

@@ -0,0 +1,197 @@
<?php
namespace IQParts\Content;
use IQParts\Content\Object\File;
use IQParts\Content\Object\Folder;
use League\Flysystem\AdapterInterface;
use League\Flysystem\Config;
use League\Flysystem\Directory;
use League\Flysystem\FileNotFoundException;
use League\Flysystem\Filesystem as FlySystem;
use League\Flysystem\Plugin\ListPaths;
/**
* Class Filesystem
* @package IQParts\Content
*/
final class Filesystem
{
/**
* @var FlySystem
*/
private $flySystem;
/**
* Filesystem constructor.
* @param AdapterInterface $adapter
* @param Config $config
*/
public function __construct(AdapterInterface $adapter, Config $config = null)
{
if (!$config) $config = [];
$flySystem = new FlySystem($adapter, $config);
$flySystem->addPlugin(new ListPaths());
$this->flySystem = $flySystem;
}
/**
* @param string $path
* @return bool
*/
public function exists(string $path): bool
{
if ($path === '/') $path = '';
return $this->flySystem->has($path);
}
/**
* @param string $path
* @return File|Folder|false
* @throws FileNotFoundException
*/
public function get(string $path)
{
/**
* If path = '/' -> create a folder object with all dirs and folders as children
*/
if ($path === '/' || $path === '') {
$dirs = [];
$files = [];
$paths = $this->flySystem->listPaths('', false);
foreach ($paths as $item) {
/** @var File|Directory $i */
$i = $this->flySystem->get($item);
if ($i->getType() === 'dir') {
$dirs[] = base64_encode($i->getPath());
} else {
$files[] = base64_encode($i->getPath());
}
}
return Folder::fromVariables(base64_encode('/'), '/', '', null, $dirs, $files);
} else {
$item = $this->flySystem->get($path);
if ($item instanceof Directory) {
return new Folder($item);
} else {
return new File($item);
}
}
}
/**
* @param string $path
* @return bool|false|resource
*/
public function getStream(string $path)
{
return $this->flySystem->readStream($path);
}
/**
* @param string $path
* @param bool $recursive
* @return \Iterator|Folder[]|File[]
*/
public function listDirContents(string $path, bool $recursive = false): \Iterator
{
if ($path === '/') $path = '';
foreach ($this->flySystem->listPaths($path, $recursive) as $path) {
$item = $this->flySystem->get($path);
if ($item instanceof Directory) {
yield new Folder($item);
} else {
yield new File($item);
}
}
}
/**
* @param string $path
* @param bool $recursive
* @return \Iterator|Folder[]
*/
public function listDirs(string $path, bool $recursive = false): \Iterator
{
if ($path === '/') $path = '';
foreach ($this->flySystem->listPaths($path, $recursive) as $path) {
$item = $this->flySystem->get($path);
if ($item instanceof Directory) {
yield new Folder($item);
}
}
}
/**
* @param string $path
* @param bool $recursive
* @return \Iterator|File[]
*/
public function listFiles(string $path, bool $recursive = false): \Iterator
{
if ($path === '/') $path = '';
foreach ($this->flySystem->listContents($path, $recursive) as $i) {
$item = $this->flySystem->get($i['path']);
if ($item instanceof \League\Flysystem\File) yield new File($item);
}
}
/**
* @param string $path
* @return bool
*/
public function delete(string $path)
{
return $this->flySystem->delete($path);
}
/**
* @param string $path
* @return bool
*/
public function deleteDir(string $path)
{
return $this->flySystem->deleteDir($path);
}
/**
* @param string $path
* @param array $config
* @return bool
*/
public function createDir(string $path, array $config = [])
{
return $this->flySystem->createDir($path, $config);
}
/**
* @param string $path
* @param $contents
* @param array $config
* @return bool
*/
public function put(string $path, $contents, array $config = []): bool
{
return $this->flySystem->put($path, $contents, $config);
}
/**
* @param string $path
* @param resource $contents
* @param array $config
* @return bool
*/
public function putStream(string $path, $contents, array $config = []): bool
{
return $this->flySystem->putStream($path, $contents, $config);
}
/**
* @return FlySystem
*/
public function getFlySystem(): FlySystem
{
return $this->flySystem;
}
}

162
src/MimeDetector.php Normal file
View File

@@ -0,0 +1,162 @@
<?php
namespace IQParts\Content;
/**
* Class MimeDetector
* @package IQParts\Framework\Content
*/
final class MimeDetector
{
/**
* @var
*/
private $mimeTypes;
/**
* Factory constructor.
* @param array $extraMimes
*/
public function __construct(array $extraMimes = [])
{
$this->mimeTypes = array_merge(
[
'doc' => 'application/msword',
'dot' => 'application/msword',
'docx' => 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
'dotx' => 'application/vnd.openxmlformats-officedocument.wordprocessingml.template',
'docm' => 'application/vnd.ms-word.document.macroEnabled.12',
'dotm' => 'application/vnd.ms-word.template.macroEnabled.12',
'xls' => 'application/vnd.ms-excel',
'xlt' => 'application/vnd.ms-excel',
'xla' => 'application/vnd.ms-excel',
'xlsx' => 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
'xltx' => 'application/vnd.openxmlformats-officedocument.spreadsheetml.template',
'xlsm' => 'application/vnd.ms-excel.sheet.macroEnabled.12',
'xltm' => 'application/vnd.ms-excel.template.macroEnabled.12',
'xlam' => 'application/vnd.ms-excel.addin.macroEnabled.12',
'xlsb' => 'application/vnd.ms-excel.sheet.binary.macroEnabled.12',
'ppt' => 'application/vnd.ms-powerpoint',
'pot' => 'application/vnd.ms-powerpoint',
'pps' => 'application/vnd.ms-powerpoint',
'ppa' => 'application/vnd.ms-powerpoint',
'pptx' => 'application/vnd.openxmlformats-officedocument.presentationml.presentation',
'potx' => 'application/vnd.openxmlformats-officedocument.presentationml.template',
'ppsx' => 'application/vnd.openxmlformats-officedocument.presentationml.slideshow',
'ppam' => 'application/vnd.ms-powerpoint.addin.macroEnabled.12',
'pptm' => 'application/vnd.ms-powerpoint.presentation.macroEnabled.12',
'potm' => 'application/vnd.ms-powerpoint.template.macroEnabled.12',
'ppsm' => 'application/vnd.ms-powerpoint.slideshow.macroEnabled.12',
'mdb' => 'application/vnd.ms-access',
'aac' => 'audio/aac',
'abw' => 'application/x-abiword',
'arc' => 'application/octet-stream',
'avi' => 'video/x-msvideo',
'azw' => 'application/vnd.amazon.ebook',
'bin' => 'application/octet-stream',
'bz' => 'application/x-bzip',
'bz2' => 'application/x-bzip2',
'csh' => 'application/x-csh',
'css' => 'text/css',
'csv' => 'text/csv',
'eot' => 'application/vnd.ms-fontobject',
'epub' => 'application/epub+zip',
'gif' => 'image/gif',
'htm' => 'text/html',
'html' => 'text/html',
'twig' => 'text/html',
'ico' => 'image/x-icon',
'ics' => 'text/calendar',
'jar' => 'application/java-archive',
'jpeg' => 'image/jpeg',
'jpg' => 'image/jpg',
'js' => 'application/javascript',
'json' => 'application/json',
'mid' => 'audio/midi',
'midi' => 'audio/midi',
'mpeg' => 'video/mpeg',
'mpkg' => 'application/vnd.apple.installer+xml',
'odp' => 'application/vnd.oasis.opendocument.presentation',
'ods' => 'application/vnd.oasis.opendocument.spreadsheet',
'odt' => 'application/vnd.oasis.opendocument.text',
'oga' => 'audio/ogg',
'ogv' => 'video/ogg',
'ogx' => 'application/ogg',
'otf' => 'font/otf',
'png' => 'image/png',
'pdf' => 'application/pdf',
'rar' => 'application/x-rar-compressed',
'rtf' => 'application/rtf',
'sh' => 'application/x-sh',
'svg' => 'image/svg+xml',
'swf' => 'application/x-shockwave-flash',
'tar' => 'application/x-tar',
'tif' => 'image/tiff',
'tiff' => 'image/tiff',
'ts' => 'application/typescript',
'txt' => 'text/plain',
'ttf' => 'font/ttf',
'vsd' => 'application/vnd.visio',
'wav' => 'audio/x-wav',
'weba' => 'audio/webm',
'webm' => 'video/webm',
'webp' => 'image/webp',
'woff' => 'font/woff',
'woff2' => 'font/woff2',
'xhtml' => 'application/xhtml+xml',
'xml' => 'application/xml',
'xul' => 'application/vnd.mozilla.xul+xml',
'zip' => 'application/zip',
'3gp' => 'video/3gpp',
'3g2' => 'video/3gpp2',
'7z' => 'application/x-7z-compressed'
],
$extraMimes
);
}
/**
* @param string $filename
* @return string
*/
public function detectFromExtension(string $filename): string
{
return $this->mimeTypes[$this->getExtension($filename)] ?? 'text/plain';
}
/**
* @param string $filename
* @return bool
*/
public function isText(string $filename): bool
{
$mime = $this->detectFromExtension($filename);
if (fnmatch('text/*', $mime)) {
return true;
}
$text = [
"application/javascript" => true,
"application/rtf" => true,
"application/json" => true,
"application/x-sh" => true,
"application/xml" => true
];
return isset($text[$mime]);
}
/**
* @param $filename
* @return string
*/
private function getExtension($filename): string
{
$file = basename($filename);
$dotPlace = strrpos($file, '.');
if ($dotPlace === false) {
return $file;
}
return substr($file, $dotPlace + 1);
}
}

View File

@@ -0,0 +1,74 @@
<?php
namespace IQParts\Content\Object;
final class CacheControl
{
public const MUST_REVALIDATE = 0;
public const NO_TRANSFORM = 1;
public const NO_CACHE = 2;
public const NO_STORE = 3;
public const PUBLIC = 4;
public const PRIVATE = 5;
public const PROXY = 6;
public const MAX_AGE = 7;
public const S_MAX_AGE = 8;
/**
* @var array
*/
private $modes = [
'must-revalidate',
'no-transform',
'no-cache',
'no-store',
'public',
'private',
'proxy-revalidate',
'max-age',
's-max-age'
];
/**
* @var int
*/
private $mode;
/**
* @var int
*/
private $seconds;
/**
* CacheControl constructor.
* @param int $mode
* @param int $seconds
*/
public function __construct(int $mode, int $seconds = -1)
{
if ($mode < 0 || $mode > 7) {
throw new \InvalidArgumentException('Invalid mode.');
}
if ($mode >= 7 && $seconds < 0) {
throw new \InvalidArgumentException('Invalid number of seconds.');
}
$this->mode = $mode;
$this->seconds = $seconds;
}
/**
* @return int|string
*/
public function __toString()
{
$mode = $this->modes[$this->mode];
if ($this->mode >= 7) {
return $mode . '=' . (string)$this->seconds;
}
return $mode;
}
}

252
src/Object/File.php Normal file
View File

@@ -0,0 +1,252 @@
<?php
declare(strict_types=1);
namespace IQParts\Content\Object;
use IQParts\Content\MimeDetector;
use League\Flysystem\File as FlyFile;
/**
* Class File
* @package IQParts\Content\Object
*/
final class File extends Item
{
/**
* @var FlyFile
*/
private $file;
/**
* @var string
*/
private $path;
/**
* @var string
*/
private $basename;
/**
* @var int
*/
private $size;
/**
* @var string
*/
private $extension;
/**
* @var string
*/
private $parent;
/**
* @var \DateTime
*/
private $date;
/**
* @var false|int
*/
private $timestamp;
/**
* @var bool
*/
private $isEditable;
/**
* PublicFile constructor.
* @param FlyFile $file
*/
public function __construct(FlyFile $file = null)
{
if ($file !== null) {
$editable = [
"js" => true,
"css" => true,
"html" => true,
"svg" => true
];
$this->file = $file;
$this->path = $file->getPath();
$this->size = $file->getSize();
$this->basename = basename($file->getPath());
$this->date = new \DateTime();
$timestamp = $file->getTimestamp();
$this->date->setTimestamp($timestamp);
$this->timestamp = $timestamp;
$dotPlace = strrpos($this->basename, '.');
if ($dotPlace !== false) $extension = substr($this->basename, $dotPlace + 1);
else $extension = '';
$this->extension = $extension;
$parentSep = strrpos($this->path, '/');
if ($parentSep !== false) {
$parent = substr($this->path, 0, $parentSep);
} else $parent = '/';
$this->parent = base64_encode($parent);
$this->isEditable = $editable[$extension] ?? false;
}
}
/**
* @return string
*/
public function getId(): string
{
return base64_encode($this->path);
}
/**
* @return FlyFile
*/
public function getFile(): FlyFile
{
return $this->file;
}
/**
* @return string
*/
public function getPath(): string
{
return $this->path;
}
/**
* @return int
*/
public function getSize(): int
{
return $this->size;
}
/**
* @return string
*/
public function getBasename(): string
{
return $this->basename;
}
/**
* @return string
*/
public function getExtension()
{
return $this->extension;
}
/**
* @return string
*/
public function getParent()
{
return $this->parent;
}
/**
* @return \DateTime
*/
public function getDate(): \DateTime
{
return $this->date;
}
/**
* @return int
*/
public function getTimestamp()
{
return $this->timestamp;
}
/**
* @return string
*/
public function getMD5(): string
{
return md5($this->file->read());
}
/**
* @return false|string
*/
public function getContents()
{
return $this->file->read();
}
/**
* @return false|resource
*/
public function getStream()
{
return $this->file->readStream();
}
/**
* @return false|string
*/
public function getMimetype()
{
return $this->file->getMimetype();
}
/**
* @return bool
*/
public function isEditable(): bool
{
return (new MimeDetector())->isText($this->basename);
}
/**
* @return array
*/
public function toArray(): array
{
return [
'basename' => $this->getBasename(),
'contents' => $this->isEditable() ? $this->getContents() : null,
'date' => $this->getDate()->format(DATE_ISO8601),
'extension' => $this->getExtension(),
'id' => $this->getId(),
'isEditable' => $this->isEditable(),
'mimetype' => $this->getMimetype(),
'md5' => $this->getMD5(),
'parent' => $this->getParent(),
'path' => $this->getPath(),
'size' => $this->getSize(),
'timestamp' => $this->getTimestamp()
];
}
/**
* @return bool
*/
function isDir(): bool
{
return false;
}
/**
* @return bool
*/
function isFile(): bool
{
return true;
}
/**
* @param string $path
* @param string $parent
* @param int $timestamp
* @return File
*/
public static function fromVariables(string $path, string $parent, int $timestamp): self
{
$file = new self;
$file->path = $path;
$file->parent = $parent;
$file->timestamp = $timestamp;
return $file;
}
}

187
src/Object/Folder.php Normal file
View File

@@ -0,0 +1,187 @@
<?php
namespace IQParts\Content\Object;
use League\Flysystem\Directory;
/**
* Class Folder
* @package IQParts\Content\Object
*/
final class Folder extends Item
{
/**
* @var Directory
*/
private $directory;
/**
* @var string
*/
private $path;
/**
* @var string|null
*/
private $parent;
/**
* @var string
*/
private $basename;
/**
* @var array
*/
private $dirs;
/**
* @var array
*/
private $files;
/**
* Folder constructor.
* @param Directory $directory
*/
public function __construct(Directory $directory = null)
{
if ($directory) {
$this->directory = $directory;
$this->path = $directory->getPath();
$filesystem = $directory->getFilesystem();
$parentSep = strrpos($this->path, '/');
if ($parentSep !== false) {
$parent = substr($this->path, 0, $parentSep);
} else $parent = '/';
$this->parent = base64_encode($parent);
$this->basename = basename($this->path);
$paths = $filesystem->listContents($this->path);
$dirs = [];
$files = [];
foreach ($paths as $path) {
$item = $filesystem->get($path['path']);
if ($item instanceof Directory) {
$dirs[] = base64_encode($item->getPath());
} else {
$files[] = base64_encode($item->getPath());
}
}
$this->dirs = $dirs;
$this->files = $files;
}
}
/**
* @return Directory
*/
public function getDirectory(): Directory
{
return $this->directory;
}
/**
* @return string
*/
public function getId(): string
{
return base64_encode($this->getPath());
}
/**
* @return string
*/
public function getPath(): string
{
return $this->path;
}
/**
* @return string|null
*/
public function getParent()
{
return $this->parent;
}
/**
* @return string
*/
public function getBasename(): string
{
return $this->basename;
}
/**
* @return array
*/
public function getDirs(): array
{
return $this->dirs;
}
/**
* @return array
*/
public function getFiles(): array
{
return $this->files;
}
/**
* @return array
*/
public function toArray(): array
{
return [
'id' => $this->getId(),
'path' => $this->getPath(),
'basename' => $this->getBasename(),
'parent' => $this->getParent(),
'dirs' => $this->getDirs(),
'files' => $this->getFiles()
];
}
/**
* @return bool
*/
function isDir(): bool
{
return true;
}
/**
* @return bool
*/
function isFile(): bool
{
return false;
}
/**
* @param string $id
* @param string $path
* @param string $basename
* @param string|null $parent
* @param array $dirs
* @param array $files
* @return Folder
*/
public static function fromVariables(
string $id,
string $path,
string $basename,
$parent = null,
array $dirs = [],
array $files = []
): Folder
{
$dir = new self();
$dir->id = $id;
$dir->path = $path;
$dir->basename = $basename;
$dir->parent = $parent;
$dir->dirs = $dirs;
$dir->files = $files;
return $dir;
}
}

20
src/Object/Item.php Normal file
View File

@@ -0,0 +1,20 @@
<?php
namespace IQParts\Content\Object;
/**
* Class Item
* @package IQParts\Content\Object
*/
abstract class Item
{
/**
* @return bool
*/
abstract function isDir(): bool;
/**
* @return bool
*/
abstract function isFile(): bool;
}

24
test/AbstractTestCase.php Normal file
View File

@@ -0,0 +1,24 @@
<?php
namespace IQParts\ContentTest;
use PHPUnit\Framework\TestCase;
/**
* Class AbstractTestCase
* @package IQParts\ContentTest
*/
abstract class AbstractTestCase extends TestCase
{
public function getTmpDirectory()
{
$location = __DIR__ . '/../tmp';
if (!file_exists($location)) {
if (!mkdir($location)) {
throw new \RuntimeException('Could not create directory: ' . $location);
}
chmod($location, 0777);
}
return $location;
}
}

View File

@@ -0,0 +1,86 @@
<?php
namespace IQParts\ContentTest\Unit;
use GuzzleHttp\Psr7\Response;
use GuzzleHttp\Psr7\Stream;
use IQParts\Content\AssetResolver;
use IQParts\Content\Filesystem;
use IQParts\Content\Object\CacheControl;
use IQParts\ContentTest\AbstractTestCase;
use League\Flysystem\Adapter\Local;
use League\Flysystem\FileNotFoundException;
use League\Flysystem\Filesystem as FlySystem;
use Psr\Http\Message\RequestInterface;
final class AssetResolverTest extends AbstractTestCase
{
/**
* @test
* @throws \ReflectionException
*/
public function testResolver()
{
$fs = new Filesystem(new Local(__DIR__ . '/../files'));
$resolver = new AssetResolver($fs);
$this->assertTrue($resolver->exists('test.txt'));
$this->assertFalse($resolver->exists('folder'));
$request = $this->createMock(RequestInterface::class);
$request
->expects($this->at(0))
->method('getRequestTarget')
->willReturn('/test.txt');
$response = new Response();
/** @var RequestInterface $request */
$response = $resolver->resolve($request, $response, new CacheControl(CacheControl::PRIVATE));
$this->assertEquals('private', $response->getHeader('Cache-Control')[0]);
$this->assertEquals('text/plain', $response->getHeader('Content-Type')[0]);
$this->assertEquals('inline; filename="test.txt";', $response->getHeader('Content-Disposition')[0]);
$this->assertEquals('0', $response->getHeader('Content-Length')[0]);
$this->assertEquals(200, $response->getStatusCode());
$this->assertInstanceOf(Stream::class, $response->getBody());
$request = $this->createMock(RequestInterface::class);
$request
->expects($this->at(0))
->method('getRequestTarget')
->willReturn('/does-not-exist.txt');
$this->expectException(FileNotFoundException::class);
$resolver->resolve($request, $response, new CacheControl(CacheControl::PRIVATE));
}
/**
* @test
*/
public function testAttachment()
{
$fs = new Filesystem(new Local(__DIR__ . '/../files'));
$resolver = new AssetResolver($fs);
$request = $this->createMock(RequestInterface::class);
$request
->expects($this->at(0))
->method('getRequestTarget')
->willReturn('/test.txt');
$response = new Response();
/** @var RequestInterface $request */
$response = $resolver->resolve($request, $response, new CacheControl(CacheControl::PRIVATE), false);
$this->assertEquals('private', $response->getHeader('Cache-Control')[0]);
$this->assertEquals('text/plain', $response->getHeader('Content-Type')[0]);
$this->assertEquals('attachment; filename="test.txt";', $response->getHeader('Content-Disposition')[0]);
$this->assertEquals('0', $response->getHeader('Content-Length')[0]);
$this->assertEquals(200, $response->getStatusCode());
$this->assertInstanceOf(Stream::class, $response->getBody());
}
}

View File

@@ -0,0 +1,51 @@
<?php
namespace IQParts\ContentTest\Unit;
use IQParts\Content\Object\CacheControl;
use IQParts\ContentTest\AbstractTestCase;
/**
* Class CacheControlTest
* @package IQParts\ContentTest\Unit
*/
final class CacheControlTest extends AbstractTestCase
{
/**
* @test
*/
public function testCacheControlWithSeconds()
{
$cacheControl = new CacheControl(CacheControl::MAX_AGE, 10);
$this->assertEquals('max-age=10', (string)$cacheControl);
}
/**
* @test
*/
public function testCacheControlWithoutSeconds()
{
$cacheControl = new CacheControl(CacheControl::PRIVATE);
$this->assertEquals('private', (string)$cacheControl);
}
/**
* @test
*/
public function testCacheControlWithInvalidSeconds()
{
$this->expectException(\InvalidArgumentException::class);
new CacheControl(CacheControl::MAX_AGE, -1);
}
/**
* @test
*/
public function testCacheControlWithInvalidMode()
{
$this->expectException(\InvalidArgumentException::class);
new CacheControl(-1);
}
}

64
test/Unit/FileTest.php Normal file
View File

@@ -0,0 +1,64 @@
<?php
namespace IQParts\ContentTest\Unit;
use IQParts\Content\Object\File;
use IQParts\ContentTest\AbstractTestCase;
use League\Flysystem\Adapter\Local;
use League\Flysystem\File as FlyFile;
use League\Flysystem\Filesystem;
/**
* Class FileTest
* @package IQParts\ContentTest\Unit
*/
final class FileTest extends AbstractTestCase
{
/**
* @return File
*/
private function createFile(bool $extension = true): File
{
$fs = new Filesystem(new Local(__DIR__ . '/../files'));
if ($extension) {
return new File($fs->get('test.txt'));
} else {
return new File($fs->get('folder/folder/no-extension'));
}
}
/**
* @test
*/
public function testFile()
{
$file = $this->createFile(true);
$this->assertEquals('txt', $file->getExtension());
$this->assertEquals('test.txt', $file->getBasename());
$this->assertEquals(base64_encode('/'), $file->getParent());
$this->assertTrue(is_int($file->getSize()));
$this->assertEquals(md5(file_get_contents(__DIR__ . '/../files/test.txt')), $file->getMD5());
$this->assertTrue($file->isEditable());
$this->assertEquals('', $file->getContents());;
$this->assertTrue(is_integer($file->getTimestamp()));
$this->assertInstanceOf(\DateTime::class, $file->getDate());
$this->assertEquals('text/plain', $file->getMimetype());
$this->assertInstanceOf(FlyFile::class, $file->getFile());
$this->assertEquals(base64_encode('test.txt'), $file->getId());
$this->assertTrue($file->isFile());
$this->assertEquals('test.txt', $file->getPath());
$this->assertTrue(is_resource($file->getStream()));
$this->assertTrue(is_array($file->toArray()));
$this->assertFalse($file->isDir());
$file = $this->createFile(false);
$this->assertEquals('', $file->getExtension());
$this->assertInstanceOf(File::class, File::fromVariables(
'folder/folder/no-extension',
'folder/folder',
time()
));
}
}

View File

@@ -0,0 +1,84 @@
<?php
namespace IQParts\ContentTest\Unit;
use IQParts\Content\Filesystem;
use IQParts\Content\Object\File;
use IQParts\Content\Object\Folder;
use IQParts\Content\Object\Item;
use League\Flysystem\Adapter\Local;
use League\Flysystem\FileNotFoundException;
use League\Flysystem\Filesystem as FlySystem;
use IQParts\ContentTest\AbstractTestCase;
/**
* Class FilesystemTest
* @package IQParts\ContentTest\Unit
*/
final class FilesystemTest extends AbstractTestCase
{
/**
* @test
*/
public function testFilesystem()
{
$fs = new Filesystem(new Local(__DIR__ . '/../files'));
$this->assertTrue($fs->exists('test.txt'));
$this->assertInstanceOf(File::class, $fs->get('test.txt'));
$this->assertInstanceOf(Folder::class, $fs->get('folder'));
$fs->put('test.txt', 'test');
$this->assertEquals('test', $fs->get('test.txt')->getContents());
$fs->put('test.txt', '');
foreach ($fs->listDirs('folder') as $dir) {
$this->assertInstanceOf(Folder::class, $dir);
}
foreach ($fs->listFiles('') as $file) {
$this->assertInstanceOf(File::class, $file);
}
$resource = $fs->getStream('test.txt');
$this->assertTrue(is_resource($resource));
$this->assertTrue($fs->putStream('test.txt', $resource));
fclose($resource);
$this->assertTrue($fs->createDir('testing'));
$this->assertTrue($fs->exists('testing'));
$fs->deleteDir('testing');
$this->assertFalse($fs->exists('testing'));
$fs->put('test2.txt', '');
$this->assertTrue($fs->exists('test2.txt'));
$fs->delete('test2.txt');
$this->assertFalse($fs->exists('test2.txt'));
$this->assertInstanceOf(FlySystem::class, $fs->getFlySystem());
$folder = $fs->get('/');
$this->assertInstanceOf(Folder::class, $folder);
$this->assertEquals([
base64_encode('test.txt')
],
$folder->getFiles()
);
$this->assertEquals(base64_encode('folder'), $folder->getDirs()[0]);
foreach ($fs->listDirs('/') as $dir) {
$this->assertInstanceOf(Folder::class, $dir);
}
foreach ($fs->listFiles('/') as $file) {
$this->assertInstanceOf(File::class, $file);
}
foreach ($fs->listDirContents('folder', false) as $item) {
$this->assertInstanceOf(Item::class, $item);
}
$this->expectException(FileNotFoundException::class);
$fs->get('does-not-exist');
}
}

43
test/Unit/FolderTest.php Normal file
View File

@@ -0,0 +1,43 @@
<?php
namespace IQParts\ContentTest\Unit;
use IQParts\Content\Object\Folder;
use IQParts\ContentTest\AbstractTestCase;
use League\Flysystem\Adapter\Local;
use League\Flysystem\Directory;
use League\Flysystem\Filesystem;
final class FolderTest extends AbstractTestCase
{
/**
* @test
*/
public function testFolder()
{
$folder = Folder::fromVariables(
base64_encode('folder'),
'folder',
'folder',
base64_encode('/'),
[],
[base64_encode('folder/test.txt')]
);
$this->assertInstanceOf(Folder::class, $folder);
$fs = new Filesystem(new Local(__DIR__ . '/../files'));
$folder = new Folder($fs->get('folder'));
$this->assertInstanceOf(Folder::class, $folder);
$this->assertEquals('folder', $folder->getBasename());
$this->assertEquals(base64_encode('/'), $folder->getParent());
$this->assertEquals([base64_encode('folder/folder')], $folder->getDirs());
$this->assertEquals([], $folder->getFiles());
$this->assertEquals('folder', $folder->getPath());
$this->assertEquals(base64_encode('folder'), $folder->getId());
$this->assertTrue($folder->isDir());
$this->assertEquals('folder', $folder->getPath());
$this->assertTrue(is_array($folder->toArray()));
$this->assertFalse($folder->isFile());
$this->assertInstanceOf(Directory::class, $folder->getDirectory());
}
}

View File

@@ -0,0 +1,22 @@
<?php
namespace IQParts\ContentTest\Unit;
use IQParts\Content\MimeDetector;
use IQParts\ContentTest\AbstractTestCase;
final class MimeDetectorTest extends AbstractTestCase
{
/**
* @test
*/
public function testMimeDetector()
{
$detector = new MimeDetector();
$this->assertTrue($detector->isText('test.txt'));
$this->assertFalse($detector->isText('test.doc'));
$this->assertEquals('application/javascript', $detector->detectFromExtension('test.js'));
$this->assertEquals('text/plain', $detector->detectFromExtension('test'));
}
}

View File

0
test/files/test.txt Normal file
View File