Skip to content

Commit

Permalink
Merge pull request #8 from chedaroo/fix-local-url-resolution
Browse files Browse the repository at this point in the history
Fix issues with local url resolution
  • Loading branch information
Dan0sz authored Jan 18, 2021
2 parents 29a75c6 + d3c7470 commit e180998
Show file tree
Hide file tree
Showing 6 changed files with 283 additions and 51 deletions.
41 changes: 41 additions & 0 deletions Model/Configuration.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
<?php

declare(strict_types=1);

namespace Dan0sz\ResourceHints\Model;

use Magento\Framework\App\Config\ScopeConfigInterface;
use Magento\Store\Model\ScopeInterface;

class Configuration implements ConfigurationInterface
{
/** @var ScopeConfigInterface $scopeConfig */
protected $scopeConfig;

/** @var Mixed $resourceHints */
private $resourceHints;

/**
* @param ScopeConfigInterface $scopeConfig
*/
public function __construct(
ScopeConfigInterface $scopeConfig
) {
$this->scopeConfig = $scopeConfig;
}

/**
* @inheritDoc
*/
public function getResourceHints($store = null)
{
if (null !== $this->resourceHints) {
return $this->resourceHints;
}
return $this->scopeConfig->getValue(
self::WEB_CONFIG_RESOURCE_HINTS,
ScopeInterface::SCOPE_STORE,
$store
);
}
}
18 changes: 18 additions & 0 deletions Model/ConfigurationInterface.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
<?php

declare(strict_types=1);

namespace Dan0sz\ResourceHints\Model;

interface ConfigurationInterface
{
const WEB_CONFIG_RESOURCE_HINTS = 'web/resource_hints/config';

/**
* Get resource hints
*
* @param null|string|bool|int|Store $store
* @return null|string|bool|int
*/
public function getResourceHints($store = null);
}
182 changes: 182 additions & 0 deletions Model/PrepareResources.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,182 @@
<?php

declare(strict_types=1);

namespace Dan0sz\ResourceHints\Model;

use Magento\Framework\Data\CollectionFactory;
use Magento\Framework\Data\Collection;
use Magento\Framework\DataObjectFactory;
use Magento\Framework\DataObject;
use Magento\Framework\Serialize\Serializer\Json;
use Magento\Framework\Serialize\JsonValidator;
use Magento\Framework\View\Asset\Repository;

class PrepareResources
{
/** @var ConfigurationInterface $configuration */
private $configuration;

/** @var CollectionFactory $collectionFactory */
private $collectionFactory;

/** @var DataObjectFactory $dataObjectFactory */
private $dataObjectFactory;

/** @var Json $json */
private $json;

/** @var JsonValidator $jsonValidator */
private $jsonValidator;

/** @var Repository $assetRepository */
private $assetRepository;

public function __construct(
ConfigurationInterface $configuration,
CollectionFactory $collectionFactory,
DataObjectFactory $dataObjectFactory,
Json $json,
JsonValidator $jsonValidator,
Repository $assetRepository
) {
$this->configuration = $configuration;
$this->collectionFactory = $collectionFactory;
$this->dataObjectFactory = $dataObjectFactory;
$this->json = $json;
$this->jsonValidator = $jsonValidator;
$this->assetRepository = $assetRepository;
}

/**
* Get collection
*
* @return mixed
*/
public function getCollection()
{
$resources = $this->getResourceHintsFromConfig();
return $this->getResourceHintsCollection($resources);
}

/**
* Get config
*
* @return null|string|bool|int
*/
private function getResourceHintsFromConfig()
{
return $this->configuration->getResourceHints();
}

/**
* Get collection
*
* @param null|string|bool|int $resourcesJson
* @return Collection
*/
private function getResourceHintsCollection($resourcesJson)
{
// Create empty collection
$collection = $this->collectionFactory->create();
// Guard against no resources
$isValid = $this->jsonValidator->isValid($resourcesJson);
if (!$isValid) return $collection;
// Parse resource hints json
$resourceHintsConfig = $this->json->unserialize($resourcesJson);
// Loop over resource hints
foreach ($resourceHintsConfig as $resource) {
// Parse resource config
$parsedResource = $this->parseResource($resource);
// Add resource to collection
$collection->addItem($parsedResource);
}
// Return collection
return $collection;
}

/**
* Parse resource
*
* @param array $resource
* @return DataObject
*/
private function parseResource($resource)
{
$resourceHint = $this->dataObjectFactory->create();
// Parse resource config
$href = $this->parseHref($resource['resource']);
$properties = $this->parseResourceProperties($resource);
// Set parsed config
$resourceHint->setData('href', $href);
$resourceHint->setData('properties', $properties);
// Return parsed config
return $resourceHint;
}

/**
* Parse resource attributes
*
* @param array $resource
* @return DataObject
*/
private function parseResourceAttributes($resource)
{
$attributes = $this->dataObjectFactory->create();
// Set rel to type
$attributes->setData('rel', $resource['type']);
// For preload
if ($resource['type'] === 'preload') {
// Set asset type
$attributes->setData('as', $resource['preload_as']);
}
// For crossorigin
if ($resource['cross_origin'] == '1') {
// Set anonymous
$attributes->setData('crossorigin', 'anonymous');
}
return $attributes;
}

/**
* Parse resource properties
*
* @param array $resource
* @return array
*/
private function parseResourceProperties($resource)
{
$attributes = $this->parseResourceAttributes($resource);
return ['attributes' => $attributes->toArray()];
}

/**
* Parse resource href
*
* @param array $resource
* @return string
*/
private function parseHref($href)
{
// Check not remote
return !$this->isRemoteAsset($href)
// Resolve local url
? $this->assetRepository->getUrl($href)
// No need to resolve, use as is
: $href;
}

/**
* Check is local asset
*
* @param string $asset
* @return int
*/
private function isRemoteAsset($asset)
{
// Regex for 'http://' or 'https://'or '//'
$re = '/^https?:\/\/.+|^\/\/.+/';
// Return number of matches
return preg_match($re, $asset);
}
}
72 changes: 22 additions & 50 deletions Observer/Framework/View/Layout/Builder.php
Original file line number Diff line number Diff line change
Expand Up @@ -8,80 +8,52 @@

namespace Dan0sz\ResourceHints\Observer\Framework\View\Layout;

use Magento\Framework\App\Config\ScopeConfigInterface as ScopeConfig;
use Dan0sz\ResourceHints\Model\PrepareResources;
use Magento\Framework\Event\Observer;
use Magento\Framework\Event\ObserverInterface;
use Magento\Framework\View\Page\Config as PageConfig;
use Magento\Framework\View\Page\Config;

class Builder implements ObserverInterface
{
const WEB_CONFIG_RESOURCE_HINTS = 'web/resource_hints/config';
/** @var PrepareResources $resources */
private $resources;

/** @var ScopeConfig $scopeConfig */
private $scopeConfig;

/** @var PageConfig $pageConfig */
/** @var Config $pageConfig */
private $pageConfig;

/**
* Builder constructor.
*
* @param ScopeConfig $scopeConfig
* @param PageConfig $pageConfig
* @param PrepareResources $configurationInterface
* @param Config $pageConfig
*/
public function __construct(
ScopeConfig $scopeConfig,
PageConfig $pageConfig
PrepareResources $prepareResources,
Config $pageConfig
) {
$this->scopeConfig = $scopeConfig;
$this->resources = $prepareResources;
$this->pageConfig = $pageConfig;
}

/**
* @param Observer $observer
* Execute
*
* @param Observer $observer
* @return $this
*/
public function execute(Observer $observer)
{
$configArray = $this->scopeConfig->getValue(self::WEB_CONFIG_RESOURCE_HINTS, ScopeConfig::SCOPE_TYPE_DEFAULT);

if (!$configArray) {
return $this;
}

$resourceHints = $this->sort((array) json_decode($configArray));

// Get resource hints
$resourceHints = $this->resources->getCollection();
// Sort resource hints
$resourceHints->setOrder('sort');
// Loop over resource hints
foreach ($resourceHints as $resource) {
$attributes = [];
$attributes['rel'] = $resource->type;

if ($resource->type == 'preload') {
$attributes['as'] = $resource->preload_as;
}

if ($resource->cross_origin == '1') {
$attributes['crossorigin'] = 'anonymous';
}

$this->pageConfig->addRemotePageAsset(
$resource->resource,
'link_rel',
[
'attributes' => $attributes
]
);
$href = $resource->getHref();
$properties = $resource->getProperties();
// Add resource hint to page
$this->pageConfig->addRemotePageAsset($href, 'link_rel', $properties);
}

return $this;
}

private function sort(array $resourceHints)
{
usort($resourceHints, function ($first, $second) {
return $first->sort_order <=> $second->sort_order;
});

return $resourceHints;
}
}
}
14 changes: 13 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

Add `<link rel='preconnect'>`, `<link rel='prefetch'>` or `<link rel='preload'>` resource hints to Magento 2's head.

## Features
## Features

- Tweak your Magento 2 store's performance by adding custom `preconnect`, `prefetch` and `preload` headers.
- Enable/disable `crossorigin` attribute.
Expand Down Expand Up @@ -39,3 +39,15 @@ If you can't or dont want to use Composer, you can download the `master`-branch
## Configuration

After installation a new tab is added to *Stores > Configuration > General > Web* called *Resource Hints*.

#### Resource

Values added to this field are assumed to be relative local assets. Their full url will be automatically resolved prior to render:

- `Vendor_Module::path/to/asset.ext` (local)
- `path/to/asset.ext` (local)

Because of this, remote assets **must** be prefixed with a protocol:
- `http://domain.tld/path/to/asset.ext` (remote)
- `https://domain.tld/path/to/asset.ext` (remote)
- `//SomeVolume/path/to/asset.ext` (remote)
7 changes: 7 additions & 0 deletions etc/di.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
<?xml version="1.0" encoding="UTF-8" ?>
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="urn:magento:framework:ObjectManager/etc/config.xsd">
<!-- Store configuration -->
<preference for="Dan0sz\ResourceHints\Model\ConfigurationInterface"
type="Dan0sz\ResourceHints\Model\Configuration" />
</config>

0 comments on commit e180998

Please sign in to comment.