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

[2.x] Refactor features class to be a kernel singleton #1647

Merged
merged 80 commits into from
Apr 8, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
80 commits
Select commit Hold shift + click to select a range
1985260
Add features property to the kernel
caendesilva Apr 1, 2024
374dfb1
Construct features on kernel construct
caendesilva Apr 1, 2024
30f30c6
Update features accessor to return kernel instance
caendesilva Apr 2, 2024
9bf80c6
Test features class is bound as singleton
caendesilva Apr 2, 2024
937e4db
Add array for the features that are enabled
caendesilva Apr 2, 2024
70392b9
Sketch out features construct to boot method
caendesilva Apr 2, 2024
9365d12
Construct features on kernel boot
caendesilva Apr 2, 2024
a5080cd
Implement features boot method
caendesilva Apr 2, 2024
1f661e9
Use only user's explicitly added features in array
caendesilva Apr 2, 2024
18395b1
Revert "Use only user's explicitly added features in array"
caendesilva Apr 2, 2024
e50e3b1
Add enabled array getter
caendesilva Apr 2, 2024
2b31543
Mark new getter as experimental
caendesilva Apr 2, 2024
5e56db8
Test the features settings
caendesilva Apr 3, 2024
419b237
Expose boot method as public internal
caendesilva Apr 3, 2024
bfa9f49
Change boot method to write property from method
caendesilva Apr 3, 2024
33ff559
Reboot features after changing config
caendesilva Apr 3, 2024
b863248
Reboot entire kernel instead of just features
caendesilva Apr 3, 2024
d2731b2
Revert "Expose boot method as public internal"
caendesilva Apr 3, 2024
dd04066
Resolve features from kernel container
caendesilva Apr 3, 2024
b33fa12
Reboot the kernel
caendesilva Apr 4, 2024
dd29e41
Remove features autobooting causing side effects
caendesilva Apr 7, 2024
6e97d3b
Instantiate features in kernel constructor
caendesilva Apr 7, 2024
c29aa76
Automatically mock config in unit tests
caendesilva Apr 7, 2024
6e12eee
Revert "Automatically mock config in unit tests"
caendesilva Apr 7, 2024
4c420a5
Move deferred booting to features class
caendesilva Apr 7, 2024
d752361
Create helper method to ensure features is booted when accessing it
caendesilva Apr 7, 2024
92c1b56
Remove old todo
caendesilva Apr 7, 2024
114c0a6
Revert "Reboot entire kernel instead of just features"
caendesilva Apr 7, 2024
485edc2
Revert "Reboot features after changing config"
caendesilva Apr 7, 2024
2156da1
Revert "Revert "Reboot features after changing config""
caendesilva Apr 7, 2024
7b0b346
Revert "Revert "Reboot entire kernel instead of just features""
caendesilva Apr 7, 2024
8430d87
Revert "Construct features on kernel boot"
caendesilva Apr 7, 2024
1783cae
Revert "Remove old todo"
caendesilva Apr 7, 2024
ccbd683
Assert same can now be used
caendesilva Apr 7, 2024
06151d9
Revert "Revert "Revert "Reboot entire kernel instead of just features"""
caendesilva Apr 7, 2024
9c085be
Revert "Revert "Expose boot method as public internal""
caendesilva Apr 7, 2024
9bd4ce0
Deprecate helper methods needing rename
caendesilva Apr 7, 2024
8804498
Handle booted state using boolean property
caendesilva Apr 7, 2024
c320c6d
Construct features on demand
caendesilva Apr 7, 2024
fa46d40
Refactor test to solve the right problems
caendesilva Apr 7, 2024
3ec67c6
Revert "Revert "Revert "Expose boot method as public internal"""
caendesilva Apr 7, 2024
9a4ccde
Revert "Handle booted state using boolean property"
caendesilva Apr 7, 2024
3aeab66
Deprecate legacy methods in internal trait
caendesilva Apr 7, 2024
999bdfc
Protect internal method only used from its own scope
caendesilva Apr 7, 2024
79deaf4
Deprecate legacy internal array
caendesilva Apr 7, 2024
64d823f
New object oriented features class handling
caendesilva Apr 7, 2024
d8af38c
Reenable accidentally implemented test
caendesilva Apr 7, 2024
9b0f019
Remove legacy static mock container
caendesilva Apr 7, 2024
096ab03
Remove unused instance mock container
caendesilva Apr 7, 2024
bd2ae0f
Inline local variables
caendesilva Apr 7, 2024
f9c8181
Annotate array generics
caendesilva Apr 7, 2024
4b77510
Simplify mock helper types
caendesilva Apr 7, 2024
148758a
Replace recursive call with wrapped array loop
caendesilva Apr 7, 2024
76a5fb0
Simplify to array method to use features instance
caendesilva Apr 7, 2024
e40e7ae
Rename `Features::sitemap()` method to `Features::hasSitemap()`
caendesilva Apr 7, 2024
ad47207
Rename `Features::rss()` method to `Features::hasRss()`
caendesilva Apr 7, 2024
5565def
Update RELEASE_NOTES.md
caendesilva Apr 8, 2024
66c84ce
Update RELEASE_NOTES.md
caendesilva Apr 8, 2024
954b7c5
Remove legacy internal mocking helpers
caendesilva Apr 8, 2024
863eebf
Add todo
caendesilva Apr 8, 2024
f93c207
Fix wrong assertion message
caendesilva Apr 8, 2024
97ac539
Extract testing helpers
caendesilva Apr 8, 2024
c267931
Convert concatenation to better formatted string interpolation
caendesilva Apr 8, 2024
6f25606
Split out unintuitive loop to standalone tests
caendesilva Apr 8, 2024
56850ce
Rename `Features::enabled()` method to `Features::has()`
caendesilva Apr 8, 2024
c2148f3
Add new enabled method
caendesilva Apr 8, 2024
ced81d0
Check enabled state using new helper
caendesilva Apr 8, 2024
f83878d
Assert using simpler helper
caendesilva Apr 8, 2024
d3ccec3
Annotate method description and generics
caendesilva Apr 8, 2024
3589da1
Move up method in class
caendesilva Apr 8, 2024
6d4097c
Move down methods in class
caendesilva Apr 8, 2024
35dc20f
Move up method blocks in class
caendesilva Apr 8, 2024
72d17a0
Change boot method data flow
caendesilva Apr 8, 2024
efba943
Refactor features getter to be static
caendesilva Apr 8, 2024
3ba4727
Inline internal mockable features class
caendesilva Apr 8, 2024
57196ac
Cleanup test
caendesilva Apr 8, 2024
fa42c6b
Revert out of scope changes in test
caendesilva Apr 8, 2024
05d67cc
Fix test boot order
caendesilva Apr 8, 2024
ccef9d8
Fix test type issue
caendesilva Apr 8, 2024
8dfb7f2
Remove old comments and make features class public
caendesilva Apr 8, 2024
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
13 changes: 13 additions & 0 deletions RELEASE_NOTES.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ This serves two purposes:

### Added
- Added a new `\Hyde\Framework\Actions\PreBuildTasks\TransferMediaAssets` build task handle media assets transfers for site builds.
- The `\Hyde\Facades\Features` class is no longer marked as internal, and is now thus part of the public API.

### Changed
- **Breaking:** The internals of the navigation system has been rewritten into a new Navigation API. This change is breaking for custom navigation implementations. For more information, see below.
Expand Down Expand Up @@ -83,6 +84,18 @@ want to adapt your code to interact with the new `InMemoryPage`, which is genera

For more information, see https://github.com/hydephp/develop/pull/1498.

## Medium impact

### Features class method renames

The following methods in the `Features` class have been renamed to follow a more consistent naming convention:

- `Features::enabled()` has been renamed to `Features::has()`
- `Features::sitemap()` has been renamed to `Features::hasSitemap()`
- `Features::rss()` has been renamed to `Features::hasRss()`

Note that this class was previously marked as internal in v1, but the change is logged here in case it was used in configuration files or custom code.

## Low impact

### Navigation internal changes
Expand Down
199 changes: 131 additions & 68 deletions packages/framework/src/Facades/Features.php
Original file line number Diff line number Diff line change
Expand Up @@ -9,93 +9,62 @@
use Hyde\Pages\DocumentationPage;
use Hyde\Support\Concerns\Serializable;
use Hyde\Support\Contracts\SerializableContract;
use Hyde\Framework\Concerns\Internal\MockableFeatures;
use Illuminate\Support\Str;

use function get_class_methods;
use function is_array;
use function array_keys;
use function array_filter;
use function extension_loaded;
use function str_starts_with;
use function in_array;
use function collect;
use function substr;
use function count;
use function app;

/**
* Allows features to be enabled and disabled in a simple object-oriented manner.
*
* @internal Until this class is split into a service/manager class, it should not be used outside of Hyde as the API is subject to change.
*
* @todo Split facade logic to service/manager class. (Initial and mock data could be set with boot/set methods)
* Based entirely on Laravel Jetstream (License MIT)
*
* @see https://jetstream.laravel.com/
*/
class Features implements SerializableContract
{
use Serializable;
use MockableFeatures;

/**
* Determine if the given specified is enabled.
* The features that are enabled.
*
* @var array<string, bool>
*/
public static function enabled(string $feature): bool
{
return static::resolveMockedInstance($feature) ?? in_array(
$feature, Config::getArray('hyde.features', static::getDefaultOptions())
);
}

// ================================================
// Determine if a given feature is enabled.
// ================================================

public static function hasHtmlPages(): bool
{
return static::enabled(static::htmlPages());
}
protected array $features = [];

public static function hasBladePages(): bool
{
return static::enabled(static::bladePages());
}

public static function hasMarkdownPages(): bool
{
return static::enabled(static::markdownPages());
}

public static function hasMarkdownPosts(): bool
public function __construct()
{
return static::enabled(static::markdownPosts());
$this->features = $this->boot();
}

public static function hasDocumentationPages(): bool
{
return static::enabled(static::documentationPages());
}

public static function hasDocumentationSearch(): bool
/**
* Determine if the given specified is enabled.
*/
public static function has(string $feature): bool
{
return static::enabled(static::documentationSearch())
&& static::hasDocumentationPages()
&& count(DocumentationPage::files()) > 0;
return in_array($feature, static::enabled());
}

public static function hasDarkmode(): bool
/**
* Get all enabled features.
*
* @return array<string>
*/
public static function enabled(): array
{
return static::enabled(static::darkmode());
return array_keys(array_filter(Hyde::features()->getFeatures()));
}

/**
* Torchlight is by default enabled automatically when an API token
* is set in the .env file but is disabled when running tests.
* Get all features and their status.
*
* @return array<string, bool>
*/
public static function hasTorchlight(): bool
public static function getFeatures(): array
{
return static::enabled(static::torchlight())
&& (Config::getNullableString('torchlight.token') !== null)
&& (app('env') !== 'testing');
return Hyde::features()->toArray();
}

// =================================================
Expand Down Expand Up @@ -142,29 +111,85 @@ public static function torchlight(): string
return 'torchlight';
}

// ================================================
// Determine if a given feature is enabled.
// ================================================

public static function hasHtmlPages(): bool
{
return static::has(static::htmlPages());
}

public static function hasBladePages(): bool
{
return static::has(static::bladePages());
}

public static function hasMarkdownPages(): bool
{
return static::has(static::markdownPages());
}

public static function hasMarkdownPosts(): bool
{
return static::has(static::markdownPosts());
}

public static function hasDocumentationPages(): bool
{
return static::has(static::documentationPages());
}

public static function hasDarkmode(): bool
{
return static::has(static::darkmode());
}

// ====================================================
// Dynamic features that in addition to being enabled
// in the config file, require preconditions to be met.
// ====================================================

/** Can a sitemap be generated? */
public static function sitemap(): bool
/**
* Can a sitemap be generated?
*/
public static function hasSitemap(): bool
{
return static::resolveMockedInstance('sitemap') ?? Hyde::hasSiteUrl()
return Hyde::hasSiteUrl()
&& Config::getBool('hyde.generate_sitemap', true)
&& extension_loaded('simplexml');
}

/** Can an RSS feed be generated? */
public static function rss(): bool
/**
* Can an RSS feed be generated?
*/
public static function hasRss(): bool
{
return static::resolveMockedInstance('rss') ?? Hyde::hasSiteUrl()
return Hyde::hasSiteUrl()
&& static::hasMarkdownPosts()
&& Config::getBool('hyde.rss.enabled', true)
&& extension_loaded('simplexml')
&& count(MarkdownPost::files()) > 0;
}

/**
* Torchlight is by default enabled automatically when an API token
* is set in the .env file but is disabled when running tests.
*/
public static function hasTorchlight(): bool
{
return static::has(static::torchlight())
&& (Config::getNullableString('torchlight.token') !== null)
&& (app('env') !== 'testing');
}

public static function hasDocumentationSearch(): bool
{
return static::has(static::documentationSearch())
&& static::hasDocumentationPages()
&& count(DocumentationPage::files()) > 0;
}

/**
* Get an array representation of the features and their status.
*
Expand All @@ -174,13 +199,10 @@ public static function rss(): bool
*/
public function toArray(): array
{
return collect(get_class_methods(static::class))
->filter(fn (string $method): bool => str_starts_with($method, 'has'))
->mapWithKeys(fn (string $method): array => [
Str::kebab(substr($method, 3)) => static::{$method}(),
])->toArray();
return $this->features;
}

/** @return array<string> */
protected static function getDefaultOptions(): array
{
return [
Expand All @@ -199,4 +221,45 @@ protected static function getDefaultOptions(): array
static::torchlight(),
];
}

protected function boot(): array
{
$options = static::getDefaultOptions();

$enabled = [];

// Set all default features to false
foreach ($options as $feature) {
$enabled[$feature] = false;
}

// Set all features to true if they are enabled in the config file
foreach ($this->getConfiguredFeatures() as $feature) {
if (in_array($feature, $options)) {
$enabled[$feature] = true;
}
}

return $enabled;
}

/** @return array<string> */
protected function getConfiguredFeatures(): array
{
return Config::getArray('hyde.features', static::getDefaultOptions());
}

/**
* @internal This method is not covered by the backward compatibility promise.
*
* @param string|array<string, bool> $feature
*/
public static function mock(string|array $feature, bool $enabled = true): void
{
$features = is_array($feature) ? $feature : [$feature => $enabled];

foreach ($features as $feature => $enabled) {
Hyde::features()->features[$feature] = $enabled;
}
}
}
6 changes: 4 additions & 2 deletions packages/framework/src/Foundation/HydeKernel.php
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ class HydeKernel implements SerializableContract
protected string $outputDirectory = '_site';
protected string $mediaDirectory = '_media';

protected Features $features;
protected Filesystem $filesystem;
protected Hyperlinks $hyperlinks;

Expand All @@ -73,6 +74,7 @@ class HydeKernel implements SerializableContract
public function __construct(?string $basePath = null)
{
$this->setBasePath($basePath ?? getcwd());

$this->filesystem = new Filesystem($this);
$this->hyperlinks = new Hyperlinks($this);

Expand All @@ -86,12 +88,12 @@ public static function version(): string

public function features(): Features
{
return new Features;
return $this->features ??= new Features();
}

public function hasFeature(string $feature): bool
{
return Features::enabled($feature);
return Features::has($feature);
}

/** @inheritDoc */
Expand Down

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -28,13 +28,13 @@ public static function make(): static
$metadata->add($item);
}

if (Features::sitemap()) {
if (Features::hasSitemap()) {
$metadata->add(Meta::link('sitemap', Hyde::url('sitemap.xml'), [
'type' => 'application/xml', 'title' => 'Sitemap',
]));
}

if (Features::rss()) {
if (Features::hasRss()) {
$metadata->add(Meta::link('alternate', Hyde::url(RssFeedGenerator::getFilename()), [
'type' => 'application/rss+xml', 'title' => RssFeedGenerator::getDescription(),
]));
Expand Down
Loading
Loading