Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Refactor XML services #648

Merged
merged 51 commits into from
Nov 6, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
51 commits
Select commit Hold shift + click to select a range
d0665a1
Remove suppression that becomes inapplicable after StyleCI
caendesilva Nov 6, 2022
acff4e2
Class properties should be camelCase
caendesilva Nov 6, 2022
c6f3982
Extract method
caendesilva Nov 6, 2022
b5d90bc
Remove test mocking for simple conditional
caendesilva Nov 6, 2022
4dc2fe1
Add codeCoverageIgnore tag
caendesilva Nov 6, 2022
1352ec8
Use throw_unless
caendesilva Nov 6, 2022
f959850
Inline method
caendesilva Nov 6, 2022
db62b70
Format code
caendesilva Nov 6, 2022
bc93846
Add noinspection tag to class
caendesilva Nov 6, 2022
9a8b8dc
Add strong types to callback function
caendesilva Nov 6, 2022
5c10d05
Extract helper method
caendesilva Nov 6, 2022
09c8c03
Move up public static function
caendesilva Nov 6, 2022
07e2bb9
Add some spacing to let the code breathe
caendesilva Nov 6, 2022
2fd13f2
Use htmlspecialchars instead of htmlentities for XML
caendesilva Nov 6, 2022
2bc6dfd
Create XML.php
caendesilva Nov 6, 2022
a6edc82
Add XML::escape function
caendesilva Nov 6, 2022
d0ad802
Use the XML helper class
caendesilva Nov 6, 2022
f0b0a29
Mark class as internal
caendesilva Nov 6, 2022
3081e07
Format code
caendesilva Nov 6, 2022
9ac7ba1
Apply fixes from StyleCI
StyleCIBot Nov 6, 2022
9e829f1
Use the XML helper class instead of local method
caendesilva Nov 6, 2022
0be1254
Move down dynamic adder
caendesilva Nov 6, 2022
d08aa76
Rename helper method
caendesilva Nov 6, 2022
5bc11c5
Use isset for consistency
caendesilva Nov 6, 2022
a1458f4
Extract method
caendesilva Nov 6, 2022
0ad8897
Add todo
caendesilva Nov 6, 2022
78e16ed
Use explicit helper instead of implicit type cast calling same method
caendesilva Nov 6, 2022
9360b9a
Extract method and add todo
caendesilva Nov 6, 2022
f3f99ac
Apply fixes from StyleCI
StyleCIBot Nov 6, 2022
67037ba
Rename RssFeedService method getDefaultOutputFilename to outputFilename
caendesilva Nov 6, 2022
b65bf7d
Merge branch 'refactor-xml-services' of github.com:hydephp/develop in…
caendesilva Nov 6, 2022
5e3ae68
Add todo
caendesilva Nov 6, 2022
4bb9813
Merge branch 'master' into refactor-xml-services
caendesilva Nov 6, 2022
da58a03
Use the site url helper instead of checking against config again
caendesilva Nov 6, 2022
4e27f50
Use the site name helper
caendesilva Nov 6, 2022
f176381
Make helpers protected
caendesilva Nov 6, 2022
77d908f
Move up public static methods
caendesilva Nov 6, 2022
8e083e5
Move up noinspection tag
caendesilva Nov 6, 2022
4b3a9f7
Import functions
caendesilva Nov 6, 2022
873955c
Remove throws tag
caendesilva Nov 6, 2022
d2acfbe
Use each instead of foreach
caendesilva Nov 6, 2022
e709e34
Convert closure to arrow function
caendesilva Nov 6, 2022
54f5b79
Rename helper method
caendesilva Nov 6, 2022
81fe29a
Inline helper method
caendesilva Nov 6, 2022
5cf534d
Reorder method calls
caendesilva Nov 6, 2022
95dc49b
Inline simple local getters
caendesilva Nov 6, 2022
f586d67
Use the Hyde URL helper instead of duplicate concatenation
caendesilva Nov 6, 2022
f8204c5
Apply fixes from StyleCI
StyleCIBot Nov 6, 2022
e42f23c
No longer mock disabled extensions
caendesilva Nov 6, 2022
0a52da7
Use throw_unless
caendesilva Nov 6, 2022
f3beb8e
Fix exception message
caendesilva Nov 6, 2022
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -15,13 +15,13 @@ class GenerateRssFeed extends BuildTask
public function run(): void
{
file_put_contents(
Hyde::sitePath(RssFeedService::getDefaultOutputFilename()),
Hyde::sitePath(RssFeedService::outputFilename()),
RssFeedService::generateFeed()
);
}

public function then(): void
{
$this->createdSiteFile('_site/'.RssFeedService::getDefaultOutputFilename())->withExecutionTime();
$this->createdSiteFile('_site/'.RssFeedService::outputFilename())->withExecutionTime();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ public static function make(): static
}

if (Features::rss()) {
$metadataBag->add(Meta::link('alternate', Hyde::url(RssFeedService::getDefaultOutputFilename()), [
$metadataBag->add(Meta::link('alternate', Hyde::url(RssFeedService::outputFilename()), [
'type' => 'application/rss+xml', 'title' => RssFeedService::getDescription(),
]));
}
Expand Down
121 changes: 53 additions & 68 deletions packages/framework/src/Framework/Services/RssFeedService.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,16 @@

namespace Hyde\Framework\Services;

use function config;
use function date;
use Exception;
use function extension_loaded;
use Hyde\Facades\Site;
use Hyde\Hyde;
use Hyde\Pages\MarkdownPost;
use Hyde\Support\Helpers\XML;
use SimpleXMLElement;
use function throw_unless;

/**
* @see \Hyde\Framework\Testing\Feature\Services\RssFeedServiceTest
Expand All @@ -21,28 +27,41 @@ class RssFeedService
{
public SimpleXMLElement $feed;

public static function generateFeed(): string
{
return (new static)->generate()->getXML();
}

public static function outputFilename(): string
{
return config('hyde.rss_filename', 'feed.xml');
}

public static function getDescription(): string
{
return XML::escape(config(
'hyde.rss_description',
XML::escape(Site::name()).' RSS Feed'
));
}

public function __construct()
{
if (! extension_loaded('simplexml') || config('testing.mock_disabled_extensions', false) === true) {
throw new Exception('The ext-simplexml extension is not installed, but is required to generate RSS feeds.');
}
throw_unless(extension_loaded('simplexml'),
new Exception('The ext-simplexml extension is not installed, but is required to generate RSS feeds.')
);

$this->feed = new SimpleXMLElement('<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/" />');
$this->feed->addChild('channel');

$this->addInitialChannelItems();
$this->addBaseChannelItems();
}

/**
* @throws \Exception
*/
public function generate(): static
{
/** @var \Hyde\Pages\MarkdownPost $post */
foreach (MarkdownPost::getLatestPosts() as $post) {
$this->addItem($post);
}
MarkdownPost::getLatestPosts()
->each(fn (MarkdownPost $post) => $this->addItem($post));

return $this;
}
Expand All @@ -56,17 +75,18 @@ protected function addItem(MarkdownPost $post): void
{
$item = $this->feed->channel->addChild('item');
$item->addChild('title', $post->title);
if ($post->canonicalUrl !== null) {
$item->addChild('link', $post->canonicalUrl);
$item->addChild('guid', $post->canonicalUrl);
}
$item->addChild('description', $post->description);

$this->addAdditionalItemData($item, $post);
$this->addDynamicItemData($item, $post);
}

protected function addAdditionalItemData(SimpleXMLElement $item, MarkdownPost $post): void
protected function addDynamicItemData(SimpleXMLElement $item, MarkdownPost $post): void
{
if (isset($post->canonicalUrl)) {
$item->addChild('link', $post->canonicalUrl);
$item->addChild('guid', $post->canonicalUrl);
}

if (isset($post->date)) {
$item->addChild('pubDate', $post->date->dateTimeObject->format(DATE_RSS));
}
Expand All @@ -81,72 +101,37 @@ protected function addAdditionalItemData(SimpleXMLElement $item, MarkdownPost $p

if (isset($post->image)) {
$image = $item->addChild('enclosure');
$image->addAttribute('url', Hyde::image((string) $post->image, true));
$image->addAttribute('type', str_ends_with($post->image->getSource(), '.png') ? 'image/png' : 'image/jpeg');
$image->addAttribute('length', (string) $post->image->getContentLength());
$image->addAttribute('url', Hyde::image($post->image->getSource(), true));
$image->addAttribute('type', $this->getImageType($post));
$image->addAttribute('length', $this->getImageLength($post));
}
}

protected function addInitialChannelItems(): void
protected function addBaseChannelItems(): void
{
$this->feed->channel->addChild('title', static::getTitle());
$this->feed->channel->addChild('link', static::getLink());
$this->feed->channel->addChild('title', XML::escape(Site::name()));
$this->feed->channel->addChild('link', XML::escape(Site::url()));
$this->feed->channel->addChild('description', static::getDescription());

$atomLink = $this->feed->channel->addChild('atom:link', namespace: 'http://www.w3.org/2005/Atom');
$atomLink->addAttribute('href', static::getLink().'/'.static::getDefaultOutputFilename());
$atomLink->addAttribute('rel', 'self');
$atomLink->addAttribute('type', 'application/rss+xml');

$this->addAdditionalChannelData();
}

protected function addAdditionalChannelData(): void
{
$this->feed->channel->addChild('language', config('site.language', 'en'));
$this->feed->channel->addChild('generator', 'HydePHP '.Hyde::version());
$this->feed->channel->addChild('lastBuildDate', date(DATE_RSS));
}

protected static function xmlEscape(string $string): string
{
return htmlspecialchars($string, ENT_XML1 | ENT_COMPAT, 'UTF-8');
}

public static function getDescription(): string
{
return static::xmlEscape(
config(
'hyde.rss_description',
static::getTitle().' RSS Feed'
)
);
}

public static function getTitle(): string
{
return static::xmlEscape(
config('site.name', 'HydePHP')
);
}

public static function getLink(): string
{
return static::xmlEscape(
rtrim(
config('site.url') ?? 'http://localhost',
'/'
)
);
$atomLink = $this->feed->channel->addChild('atom:link', namespace: 'http://www.w3.org/2005/Atom');
$atomLink->addAttribute('href', XML::escape(Hyde::url(static::outputFilename())));
$atomLink->addAttribute('rel', 'self');
$atomLink->addAttribute('type', 'application/rss+xml');
}

public static function getDefaultOutputFilename(): string
protected function getImageType(MarkdownPost $post): string
{
return config('hyde.rss_filename', 'feed.xml');
/** @todo Add support for more types */
return str_ends_with($post->image->getSource(), '.png') ? 'image/png' : 'image/jpeg';
}

public static function generateFeed(): string
protected function getImageLength(MarkdownPost $post): string
{
return (new static)->generate()->getXML();
/** @todo We might want to add a build warning if the length is zero */
return (string) $post->image->getContentLength();
}
}
50 changes: 33 additions & 17 deletions packages/framework/src/Framework/Services/SitemapService.php
Original file line number Diff line number Diff line change
@@ -1,19 +1,28 @@
<?php

declare(strict_types=1);

/** @noinspection PhpComposerExtensionStubsInspection */

declare(strict_types=1);

namespace Hyde\Framework\Services;

use function config;
use function date;
use Exception;
use function extension_loaded;
use function filemtime;
use Hyde\Hyde;
use Hyde\Pages\BladePage;
use Hyde\Pages\DocumentationPage;
use Hyde\Pages\MarkdownPage;
use Hyde\Pages\MarkdownPost;
use Hyde\Support\Helpers\XML;
use Hyde\Support\Models\Route;
use function in_array;
use function microtime;
use function round;
use SimpleXMLElement;
use function throw_unless;

/**
* @see \Hyde\Framework\Testing\Feature\Services\SitemapServiceTest
Expand All @@ -23,23 +32,28 @@
class SitemapService
{
public SimpleXMLElement $xmlElement;
protected float $time_start;
protected float $timeStart;

public static function generateSitemap(): string
{
return (new static)->generate()->getXML();
}

public function __construct()
{
if (! extension_loaded('simplexml') || config('testing.mock_disabled_extensions', false) === true) {
throw new Exception('The ext-simplexml extension is not installed, but is required to generate RSS feeds.');
}
throw_unless(extension_loaded('simplexml'),
new Exception('The ext-simplexml extension is not installed, but is required to generate sitemaps.')
);

$this->time_start = microtime(true);
$this->timeStart = microtime(true);

$this->xmlElement = new SimpleXMLElement('<?xml version="1.0" encoding="UTF-8"?><urlset xmlns="https://www.sitemaps.org/schemas/sitemap/0.9"></urlset>');
$this->xmlElement->addAttribute('generator', 'HydePHP '.Hyde::version());
}

public function generate(): static
{
\Hyde\Facades\Route::all()->each(function ($route) {
Route::all()->each(function (Route $route): void {
$this->addRoute($route);
});

Expand All @@ -48,27 +62,29 @@ public function generate(): static

public function getXML(): string
{
$this->xmlElement->addAttribute('processing_time_ms', (string) round((microtime(true) - $this->time_start) * 1000, 2));
$this->xmlElement->addAttribute('processing_time_ms', $this->getFormattedProcessingTime());

return (string) $this->xmlElement->asXML();
}

public function addRoute(Route $route): void
{
$urlItem = $this->xmlElement->addChild('url');
$urlItem->addChild('loc', htmlentities(Hyde::url($route->getOutputPath())));
$urlItem->addChild('lastmod', htmlentities($this->getLastModDate($route->getSourcePath())));

$urlItem->addChild('loc', XML::escape(Hyde::url($route->getOutputPath())));
$urlItem->addChild('lastmod', XML::escape($this->getLastModDate($route->getSourcePath())));
$urlItem->addChild('changefreq', 'daily');

if (config('hyde.sitemap.dynamic_priority', true)) {
$urlItem->addChild('priority', $this->getPriority($route->getPageClass(), $route->getPage()->getIdentifier()));
$urlItem->addChild('priority', $this->getPriority(
$route->getPageClass(), $route->getPage()->getIdentifier()
));
}
}

protected function getLastModDate(string $file): string
{
return date('c', filemtime(
$file
));
return date('c', filemtime($file));
}

protected function getPriority(string $pageClass, string $slug): string
Expand Down Expand Up @@ -96,8 +112,8 @@ protected function getPriority(string $pageClass, string $slug): string
return (string) $priority;
}

public static function generateSitemap(): string
protected function getFormattedProcessingTime(): string
{
return (new static)->generate()->getXML();
return (string) round((microtime(true) - $this->timeStart) * 1000, 2);
}
}
18 changes: 18 additions & 0 deletions packages/framework/src/Support/Helpers/XML.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
<?php

declare(strict_types=1);

namespace Hyde\Support\Helpers;

use function htmlspecialchars;

/**
* @internal This class is currently experimental and may change without notice.
*/
class XML
{
public static function escape(string $string): string
{
return htmlspecialchars($string, ENT_XML1 | ENT_COMPAT, 'UTF-8');
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
* Class BuildOutputDirectoryCanBeChangedTest.
*
* @todo add test for the Rebuild Service
* @todo this unit test should be moved to the unit test suite
*/
class BuildOutputDirectoryCanBeChangedTest extends TestCase
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,15 +20,6 @@ public function test_service_instantiates_xml_element()
$this->assertInstanceOf('SimpleXMLElement', $service->feed);
}

public function test_constructor_throws_exception_when_missing_simplexml_extension()
{
config(['testing.mock_disabled_extensions' => true]);

$this->expectException(\Exception::class);
$this->expectExceptionMessage('The ext-simplexml extension is not installed, but is required to generate RSS feeds.');
new RssFeedService();
}

public function test_xml_root_element_is_set_to_rss_2_0()
{
$service = new RssFeedService();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,15 +30,6 @@ public function test_service_instantiates_xml_element()
$this->assertInstanceOf('SimpleXMLElement', $service->xmlElement);
}

public function test_constructor_throws_exception_when_missing_simplexml_extension()
{
config(['testing.mock_disabled_extensions' => true]);

$this->expectException(\Exception::class);
$this->expectExceptionMessage('The ext-simplexml extension is not installed, but is required to generate RSS feeds.');
new SitemapService();
}

public function test_generate_adds_default_pages_to_xml()
{
$service = new SitemapService();
Expand Down