luyadev/luya-aws

View on GitHub
src/AssetManager.php

Summary

Maintainability
A
0 mins
Test Coverage
F
47%
<?php

namespace luya\aws;

use luya\helpers\Inflector;
use luya\traits\CacheableTrait;
use luya\web\AssetManager as WebAssetManager;
use Yii;

/**
 * An S3 compatible Asset Manager.
 *
 * Saves all the files in the s3 system and serves them from the CDN url. Uploading is done once, afterwards urls are served from the cache
 * which makes it fast and no disk io is required!
 *
 * > Using this AssetManager requires you to correctly adjust the app version for each deployment or building the app with a CI system
 * > which runns composer install before each deployment (building of the docker images). It also requires an enabled cache system otherwise
 * > this implementation is rather slow compared to the original AssetManager.
 *
 * By default, an asset folder and its version folder will be created when receiving the first web request. The version folder is based
 * on the composer.lock file timestamp (Yii::$app->packageInstaller->timestamp) and the application version (Yii::$app->version). Afterwarts
 * the path to the uploaded asset file will be stored in the cache, it is therfore required to have caching enabled. When you scale with multiple
 * instances of the same website, ensure the caching system shares its data inbetween.
 *
 * Setup the AssetManager as component in your config. Of course ensure that luya aws storage is setup as storage system. See {{S3FileSystem}}.
 *
 * ```php
 * 'assetManager' => [
 *     'class' => 'luya\aws\AssetManager',
 * ],
 * ```
 *
 * > WOFF/FONT needs a valid CORS request to be loaded from a remote CDN!
 *
 * CORS Policy:
 *
 * ```json
 * [
 *   {
 *       "AllowedHeaders": [
 *           "*"
 *       ],
 *       "AllowedMethods": [
 *           "GET",
 *           "POST"
 *       ],
 *       "AllowedOrigins": [
 *           "http://luyaenvdev-web-luya-env-dev.dev.zephir.ch",
 *           "*",
 *           "localhost"
 *       ],
 *       "ExposeHeaders": [],
 *       "MaxAgeSeconds": 3000
 *   }
 * ]
 * ```
 *
 * Compared to the original AssetManger the following properties has no effect!
 *
 * + hashCallback
 * + linkAssets
 * + dirMode
 * + fileMode
 * + beforeCopy
 *
 * @property string $versionPath The version path which should be used between builds. By defaults its a combination of
 * the vendor timestamp and the Yii::$app->version. The versionPath will be append as folder like `assets/<VERSION_PATH>/1hf3ufh`.
 * Where `assets` is taken from the $basePath property.
 *
 * @see Inspiration taken from https://gitlab.com/mikk150/yii2-asset-manager-flysystem
 * @since 1.4.0
 * @author Basil Suter <git@nadar.io>
 */
class AssetManager extends WebAssetManager
{
    use CacheableTrait;

    /**
     * All assets will be stored using this path inside the bucket, for root storage use null
     *
     * @var string
     */
    public $basePath = 'assets';

    /**
     * {@inheritDoc}
     */
    public function init()
    {
        $this->hashCallback = function ($path) {
            return sprintf('%x', crc32($path));
        };
    }

    private $_versionPath;

    /**
     * Setter method of the path name
     *
     * An example using only the app version would be:
     *
     * ```php
     * 'versionPath' => function() {
     *    return Yii::$app->version;
     * }
     * ```
     *
     * @param string|callable $path
     */
    public function setVersionPath($path)
    {
        if (is_callable($path)) {
            $path = call_user_func($path);
        }

        $this->_versionPath = $path;
    }

    /**
     * Getter method for versionPath
     *
     * @return string
     */
    public function getVersionPath()
    {
        if ($this->_versionPath === null) {
            $this->_versionPath = Inflector::slug(Yii::$app->formatter->asDatetime(Yii::$app->packageInstaller->timestamp, 'yyyyMMddHHmmss') . '-' . Yii::$app->version);
        }

        return $this->_versionPath;
    }

    /**
     * Override base path permission check as this check should not be done.
     *
     * @return void
     */
    public function checkBasePathPermission()
    {
    }

    /**
     * Publish a given file with its directory
     *
     * {@inheritDoc}
     */
    protected function publishFile($src)
    {
        $dir = $this->hash($src);
        $fileName = basename($src);
        $dstDir = $this->basePath . DIRECTORY_SEPARATOR . $this->getVersionPath() . DIRECTORY_SEPARATOR . $dir; // assets/<versionpath>/<hash>
        $dstFile = $dstDir . DIRECTORY_SEPARATOR . $fileName; // assets/<versionpath>/<hash>/jquery.js

        if ($cached = $this->isCached($this->forceCopy, $dstFile)) {
            return $cached;
        }

        if ($this->forceCopy || !Yii::$app->storage->fileSystemExists($dstFile)) {
            Yii::$app->storage->fileSystemSaveFile($src, $dstFile);
        }

        // the path and the URL that the asset is published as.
        return $this->setCached($this->forceCopy, $dstFile, Yii::$app->storage->fileHttpPath($dstFile));
    }

    /**
     * Publish a directly with all its file
     *
     * {@inheritDoc}
     */
    protected function publishDirectory($src, $options)
    {
        $dir = $this->hash($src);
        $dstDir = $this->basePath . DIRECTORY_SEPARATOR . $this->getVersionPath() . DIRECTORY_SEPARATOR . $dir; // assets/<versionpath>/<hash>

        $forceCopy = $this->forceCopy || (isset($options['forceCopy']) && $options['forceCopy']);

        if ($cached = $this->isCached($forceCopy, $dstDir)) {
            return $cached;
        }

        if ($forceCopy || !Yii::$app->storage->fileSystemFolderExists($dstDir)) {
            Yii::$app->storage->folderTransfer($src, $dstDir);
        }

        return $this->setCached($forceCopy, $dstDir, Yii::$app->storage->fileHttpPath($dstDir));
    }

    /**
     * Check if a cacheable value exists and is valid.
     *
     * @param boolean $forceCopy
     * @param string $dst
     * @return boolean|array
     */
    private function isCached($forceCopy, $dst)
    {
        if ($forceCopy) {
            return false;
        }

        $cacheValue = $this->getHasCache(['assetManager', $dst]);

        if (!$cacheValue) {
            return false;
        }

        return [$dst, $cacheValue];
    }

    /**
     * Set the values into cache if allowed and return expected format
     *
     * @param boolean $forceCopy
     * @param string $dst
     * @param string $cdnPath
     * @return array
     */
    private function setCached($forceCopy, $dst, $cdnPath)
    {
        if (!$forceCopy) {
            $this->setHasCache(['assetManager', $dst], $cdnPath, null, 0);
        }

        return [$dst, $cdnPath];
    }
}