From e5d05868dc1686accdbbe31b2cd249456a58b844 Mon Sep 17 00:00:00 2001 From: github-actions Date: Sun, 30 Jun 2024 16:57:55 +0000 Subject: [PATCH 1/8] Merge pull request #1765 from hydephp/sync-packages Internal: Sync changes from downstream packages into the monorepo https://github.com/hydephp/develop/commit/db72e6041e35774a8bd5e05d344c36900790d725 --- .github/workflows/run-tests.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/run-tests.yml b/.github/workflows/run-tests.yml index 90bd603e..76b1488c 100644 --- a/.github/workflows/run-tests.yml +++ b/.github/workflows/run-tests.yml @@ -47,7 +47,7 @@ jobs: else rsync -a --exclude=hyde ./. ./hyde/packages/hyde/framework/src fi - + - name: Update composer.json to load framework from local source run: | cd hyde From fc482470137c4bae5831d930a69967919593c46e Mon Sep 17 00:00:00 2001 From: github-actions Date: Sun, 30 Jun 2024 18:04:09 +0000 Subject: [PATCH 2/8] Merge pull request #1767 from hydephp/add-version-prefix-to-the-generator-string Add a version prefix to the sitemap generator version attribute https://github.com/hydephp/develop/commit/b340e9a05e056f8db96cdecfc24bf49dcf9ac0ee --- src/Framework/Features/XmlGenerators/SitemapGenerator.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Framework/Features/XmlGenerators/SitemapGenerator.php b/src/Framework/Features/XmlGenerators/SitemapGenerator.php index 38e53a3a..b536141c 100644 --- a/src/Framework/Features/XmlGenerators/SitemapGenerator.php +++ b/src/Framework/Features/XmlGenerators/SitemapGenerator.php @@ -52,7 +52,7 @@ protected function constructBaseElement(): void $this->startClock(); $this->xmlElement = new SimpleXMLElement(''); - $this->xmlElement->addAttribute('generator', 'HydePHP '.Hyde::version()); + $this->xmlElement->addAttribute('generator', 'HydePHP v'.Hyde::version()); } protected function addRoute(Route $route): void From d265453a05ed764a8368ff5f16073a0b654752a0 Mon Sep 17 00:00:00 2001 From: github-actions Date: Sun, 30 Jun 2024 18:40:45 +0000 Subject: [PATCH 3/8] Merge pull request #1769 from hydephp/fix-source-code-formatting Fix source code formatting https://github.com/hydephp/develop/commit/c969ca075e368f16b8ab7d3dbb12ce3532574143 --- .../Services/Markdown/CodeblockFilepathProcessorTest.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/Feature/Services/Markdown/CodeblockFilepathProcessorTest.php b/tests/Feature/Services/Markdown/CodeblockFilepathProcessorTest.php index 7c3283ac..96cd85e6 100644 --- a/tests/Feature/Services/Markdown/CodeblockFilepathProcessorTest.php +++ b/tests/Feature/Services/Markdown/CodeblockFilepathProcessorTest.php @@ -61,7 +61,7 @@ public function testFilepathPatternIsCaseInsensitive() public function testPreprocessCanUseHtmlComments() { $markdown = <<<'MD' - + ```html

Hello World

From aa88d4d92836cd287fa18eec6013afcfe0ea0b3a Mon Sep 17 00:00:00 2001 From: github-actions Date: Mon, 1 Jul 2024 13:52:50 +0000 Subject: [PATCH 4/8] Merge pull request #1770 from hydephp/improve-yaml-configuration-loading Improve the Yaml configuration loader https://github.com/hydephp/develop/commit/f8f60cd6cdf6de48801e6b3db5d6f149669d7618 --- .../Internal/LoadYamlConfiguration.php | 75 +++++++--- tests/Feature/LoadYamlConfigurationTest.php | 132 +++++++++++++++--- 2 files changed, 168 insertions(+), 39 deletions(-) diff --git a/src/Foundation/Internal/LoadYamlConfiguration.php b/src/Foundation/Internal/LoadYamlConfiguration.php index faf25ce3..77b4b439 100644 --- a/src/Foundation/Internal/LoadYamlConfiguration.php +++ b/src/Foundation/Internal/LoadYamlConfiguration.php @@ -17,18 +17,23 @@ /** * @internal Bootstrap service that loads the YAML configuration file. * + * @implements \LaravelZero\Framework\Contracts\BoostrapperContract [sic] + * * @see docs/digging-deeper/customization.md#yaml-configuration * * It also supports loading multiple configuration namespaces, where a configuration namespace is defined - * as the first level in the service container configuration repository array, and usually corresponds - * one-to-one with a file in the config directory. This feature, by design, requires a top-level - * configuration entry to be present as 'hyde' in the YAML file. Existing config files - * will be parsed as normal, but can be migrated by indenting all entries by one - * level, and adding a top-level 'hyde' key. Then additional namespaces can - * be added underneath as needed. + * as a firs level entry in the service container configuration repository array, and corresponds + * one-to-one with a file in the config directory, and a root-level key in the YAML file. + * + * This feature, by design, requires a top-level configuration entry to be present as 'hyde' in the YAML file. + * Existing config files will be parsed as normal, but can be migrated by indenting all entries by one level, + * and adding a top-level 'hyde' key. Then additional namespaces can be added underneath as needed. */ class LoadYamlConfiguration { + protected array $config; + protected array $yaml; + /** * Performs a core task that needs to be performed on * early stages of the framework. @@ -36,7 +41,15 @@ class LoadYamlConfiguration public function bootstrap(): void { if ($this->hasYamlConfigFile()) { + $this->config = Config::all(); + $this->yaml = $this->parseYamlFile(); + + $this->supportSettingSidebarHeaderFromSiteName(); + $this->supportSettingRssFeedTitleFromSiteName(); + $this->mergeParsedConfiguration(); + + Config::set($this->config); } } @@ -46,8 +59,8 @@ protected function hasYamlConfigFile(): bool || file_exists(Hyde::path('hyde.yaml')); } - /** @return array */ - protected function getYaml(): array + /** @return array */ + protected function parseYamlFile(): array { return Arr::undot((array) Yaml::parse(file_get_contents($this->getFile()))); } @@ -61,33 +74,55 @@ protected function getFile(): string protected function mergeParsedConfiguration(): void { - $yaml = $this->getYaml(); + $yaml = $this->yaml; // If the Yaml file contains namespaces, we merge those using more granular logic // that only applies the namespace data to each configuration namespace. - if ($this->configurationContainsNamespaces($yaml)) { + if ($this->configurationContainsNamespaces()) { /** @var array> $yaml */ foreach ($yaml as $namespace => $data) { $this->mergeConfiguration($namespace, Arr::undot((array) $data)); } - - return; + } else { + // Otherwise, we can merge using the default strategy, which is simply applying all the data to the hyde namespace. + $this->mergeConfiguration('hyde', $yaml); } - - // Otherwise, we can merge using the default strategy, which is simply applying all the data to the hyde namespace. - $this->mergeConfiguration('hyde', $yaml); } protected function mergeConfiguration(string $namespace, array $yamlData): void { - Config::set($namespace, array_merge( - Config::getArray($namespace, []), + $this->config[$namespace] = array_merge( + $this->config[$namespace] ?? [], $yamlData - )); + ); + } + + protected function configurationContainsNamespaces(): bool + { + return array_key_first($this->yaml) === 'hyde'; + } + + private function supportSettingSidebarHeaderFromSiteName(): void + { + $sidebarHeaderIsNotSetInPhpConfig = ($this->config['docs']['sidebar']['header'] ?? null) === 'HydePHP Docs'; + $siteNameFromYaml = $this->configurationContainsNamespaces() ? ($this->yaml['hyde']['name'] ?? null) : ($this->yaml['name'] ?? null); + + if ($sidebarHeaderIsNotSetInPhpConfig) { + if ($siteNameFromYaml !== null) { + $this->config['docs']['sidebar']['header'] = $siteNameFromYaml.' Docs'; + } + } } - protected function configurationContainsNamespaces(array $yaml): bool + private function supportSettingRssFeedTitleFromSiteName(): void { - return array_key_first($yaml) === 'hyde'; + $rssFeedTitleIsNotSetInPhpConfig = ($this->config['hyde']['rss']['description'] ?? null) === 'HydePHP RSS Feed'; + $siteNameFromYaml = $this->configurationContainsNamespaces() ? ($this->yaml['hyde']['name'] ?? null) : ($this->yaml['name'] ?? null); + + if ($rssFeedTitleIsNotSetInPhpConfig) { + if ($siteNameFromYaml !== null) { + $this->config['hyde']['rss']['description'] = $siteNameFromYaml.' RSS Feed'; + } + } } } diff --git a/tests/Feature/LoadYamlConfigurationTest.php b/tests/Feature/LoadYamlConfigurationTest.php index 5cc6edec..efd8ef56 100644 --- a/tests/Feature/LoadYamlConfigurationTest.php +++ b/tests/Feature/LoadYamlConfigurationTest.php @@ -15,8 +15,6 @@ class LoadYamlConfigurationTest extends TestCase { public function testCanDefineHydeConfigSettingsInHydeYmlFile() { - config(['hyde' => []]); - $this->file('hyde.yml', <<<'YAML' name: HydePHP url: "http://localhost" @@ -44,9 +42,6 @@ public function testCanDefineHydeConfigSettingsInHydeYmlFile() public function testCanDefineMultipleConfigSettingsInHydeYmlFile() { - config(['hyde' => []]); - config(['docs' => []]); - $this->file('hyde.yml', <<<'YAML' hyde: name: HydePHP @@ -125,8 +120,6 @@ public function testConfigurationOptionsAreMerged() public function testCanAddConfigurationOptionsInNamespacedArray() { - config(['hyde' => []]); - $this->file('hyde.yml', <<<'YAML' hyde: name: HydePHP @@ -134,6 +127,7 @@ public function testCanAddConfigurationOptionsInNamespacedArray() bar: baz: qux YAML); + $this->runBootstrapper(); $this->assertSame('HydePHP', Config::get('hyde.name')); @@ -143,14 +137,13 @@ public function testCanAddConfigurationOptionsInNamespacedArray() public function testCanAddArbitraryNamespacedData() { - config(['hyde' => []]); - $this->file('hyde.yml', <<<'YAML' hyde: some: thing foo: bar: baz YAML); + $this->runBootstrapper(); $this->assertSame('baz', Config::get('foo.bar')); @@ -158,12 +151,11 @@ public function testCanAddArbitraryNamespacedData() public function testAdditionalNamespacesRequireTheHydeNamespaceToBePresent() { - config(['hyde' => []]); - $this->file('hyde.yml', <<<'YAML' foo: bar: baz YAML); + $this->runBootstrapper(); $this->assertNull(Config::get('foo.bar')); @@ -171,14 +163,13 @@ public function testAdditionalNamespacesRequireTheHydeNamespaceToBePresent() public function testAdditionalNamespacesRequiresHydeNamespaceToBeTheFirstEntry() { - config(['hyde' => []]); - $this->file('hyde.yml', <<<'YAML' foo: bar: baz hyde: some: thing YAML); + $this->runBootstrapper(); $this->assertNull(Config::get('foo.bar')); @@ -186,13 +177,12 @@ public function testAdditionalNamespacesRequiresHydeNamespaceToBeTheFirstEntry() public function testHydeNamespaceCanBeEmpty() { - config(['hyde' => []]); - $this->file('hyde.yml', <<<'YAML' hyde: foo: bar: baz YAML); + $this->runBootstrapper(); $this->assertSame('baz', Config::get('foo.bar')); @@ -201,13 +191,12 @@ public function testHydeNamespaceCanBeEmpty() public function testHydeNamespaceCanBeNull() { // This is essentially the same as the empty state test above, at least according to the YAML spec. - config(['hyde' => []]); - $this->file('hyde.yml', <<<'YAML' hyde: null foo: bar: baz YAML); + $this->runBootstrapper(); $this->assertSame('baz', Config::get('foo.bar')); @@ -215,13 +204,12 @@ public function testHydeNamespaceCanBeNull() public function testHydeNamespaceCanBlank() { - config(['hyde' => []]); - $this->file('hyde.yml', <<<'YAML' hyde: '' foo: bar: baz YAML); + $this->runBootstrapper(); $this->assertSame('baz', Config::get('foo.bar')); @@ -234,6 +222,7 @@ public function testDotNotationCanBeUsed() $this->file('hyde.yml', <<<'YAML' foo.bar.baz: qux YAML); + $this->runBootstrapper(); $this->assertSame(['foo' => ['bar' => ['baz' => 'qux']]], Config::get('hyde')); @@ -254,14 +243,119 @@ public function testDotNotationCanBeUsedWithNamespaces() two: foo.bar.baz: qux YAML); + $this->runBootstrapper(); $expected = ['foo' => ['bar' => ['baz' => 'qux']]]; + $this->assertSame($expected, Config::get('hyde')); $this->assertSame($expected, Config::get('one')); $this->assertSame($expected, Config::get('two')); } + public function testSettingSiteNameSetsSidebarHeader() + { + $this->file('hyde.yml', <<<'YAML' + name: Example + YAML); + + $this->runBootstrapper(); + + $this->assertSame('Example Docs', Config::get('docs.sidebar.header')); + } + + public function testSettingSiteNameSetsSidebarHeaderWhenUsingHydeNamespace() + { + $this->file('hyde.yml', <<<'YAML' + hyde: + name: Example + YAML); + + $this->runBootstrapper(); + + $this->assertSame('Example Docs', Config::get('docs.sidebar.header')); + } + + public function testSettingSiteNameSetsSidebarHeaderUnlessAlreadySpecifiedInYamlConfig() + { + $this->file('hyde.yml', <<<'YAML' + hyde: + name: Example + docs: + sidebar: + header: Custom + YAML); + + $this->runBootstrapper(); + + $this->assertSame('Custom', Config::get('docs.sidebar.header')); + } + + public function testSettingSiteNameSetsSidebarHeaderUnlessAlreadySpecifiedInStandardConfig() + { + config(['docs.sidebar.header' => 'Custom']); + + $this->file('hyde.yml', <<<'YAML' + hyde: + name: Example + YAML); + + $this->runBootstrapper(); + + $this->assertSame('Custom', Config::get('docs.sidebar.header')); + } + + public function testSettingSiteNameSetsRssFeedSiteName() + { + $this->file('hyde.yml', <<<'YAML' + name: Example + YAML); + + $this->runBootstrapper(); + + $this->assertSame('Example RSS Feed', Config::get('hyde.rss.description')); + } + + public function testSettingSiteNameSetsRssFeedSiteNameWhenUsingHydeNamespace() + { + $this->file('hyde.yml', <<<'YAML' + hyde: + name: Example + YAML); + + $this->runBootstrapper(); + + $this->assertSame('Example RSS Feed', Config::get('hyde.rss.description')); + } + + public function testSettingSiteNameSetsRssFeedSiteNameUnlessAlreadySpecifiedInYamlConfig() + { + $this->file('hyde.yml', <<<'YAML' + hyde: + name: Example + rss: + description: Custom + YAML); + + $this->runBootstrapper(); + + $this->assertSame('Custom', Config::get('hyde.rss.description')); + } + + public function testSettingSiteNameSetsRssFeedSiteNameUnlessAlreadySpecifiedInStandardConfig() + { + config(['hyde.rss.description' => 'Custom']); + + $this->file('hyde.yml', <<<'YAML' + hyde: + name: Example + YAML); + + $this->runBootstrapper(); + + $this->assertSame('Custom', Config::get('hyde.rss.description')); + } + protected function runBootstrapper(): void { $this->app->bootstrapWith([LoadYamlConfiguration::class]); From 0d626968cadc245b0ef05aaa3aac78afda0c1ae8 Mon Sep 17 00:00:00 2001 From: github-actions Date: Mon, 1 Jul 2024 14:23:41 +0000 Subject: [PATCH 5/8] Merge pull request #1772 from hydephp/refactor-console-kernel-bootstrapper-loading Refactor console kernel bootstrapper loading https://github.com/hydephp/develop/commit/034b151b34b8d26f3de027fb94aeb0952bcdd99c --- src/Foundation/ConsoleKernel.php | 27 +++++++--------- tests/Feature/ConsoleKernelTest.php | 49 ++++++++++++++++++++--------- 2 files changed, 46 insertions(+), 30 deletions(-) diff --git a/src/Foundation/ConsoleKernel.php b/src/Foundation/ConsoleKernel.php index fdad24e0..a4fee7af 100644 --- a/src/Foundation/ConsoleKernel.php +++ b/src/Foundation/ConsoleKernel.php @@ -5,12 +5,6 @@ namespace Hyde\Foundation; use LaravelZero\Framework\Kernel; -use Hyde\Foundation\Internal\LoadYamlConfiguration; - -use function array_combine; -use function array_splice; -use function array_values; -use function tap; class ConsoleKernel extends Kernel { @@ -19,17 +13,20 @@ class ConsoleKernel extends Kernel */ protected function bootstrappers(): array { - $bootstrappers = $this->bootstrappers; - - // Insert our bootstrapper between load configuration and register provider bootstrappers. - array_splice($bootstrappers, 5, 0, LoadYamlConfiguration::class); - // Since we store our application config in `app/config.php`, we need to replace // the default LoadConfiguration bootstrapper class with our implementation. // We do this by swapping out the LoadConfiguration class with our own. - - return array_values((array) tap(array_combine($bootstrappers, $bootstrappers), function (array &$array): void { - $array[\LaravelZero\Framework\Bootstrap\LoadConfiguration::class] = \Hyde\Foundation\Internal\LoadConfiguration::class; - })); + // We also inject our Yaml configuration loading bootstrapper. + + return [ + \LaravelZero\Framework\Bootstrap\CoreBindings::class, + \LaravelZero\Framework\Bootstrap\LoadEnvironmentVariables::class, + \Hyde\Foundation\Internal\LoadConfiguration::class, + \Illuminate\Foundation\Bootstrap\HandleExceptions::class, + \LaravelZero\Framework\Bootstrap\RegisterFacades::class, + \Hyde\Foundation\Internal\LoadYamlConfiguration::class, + \LaravelZero\Framework\Bootstrap\RegisterProviders::class, + \Illuminate\Foundation\Bootstrap\BootProviders::class, + ]; } } diff --git a/tests/Feature/ConsoleKernelTest.php b/tests/Feature/ConsoleKernelTest.php index 0ff66336..6c433bdb 100644 --- a/tests/Feature/ConsoleKernelTest.php +++ b/tests/Feature/ConsoleKernelTest.php @@ -4,6 +4,7 @@ namespace Hyde\Framework\Testing\Feature; +use LaravelZero\Framework\Kernel as LaravelZeroKernel; use Hyde\Foundation\Internal\LoadYamlConfiguration; use Illuminate\Contracts\Console\Kernel; use Hyde\Foundation\ConsoleKernel; @@ -11,7 +12,13 @@ use ReflectionMethod; /** + * This test covers our custom console kernel, which is responsible for registering our custom bootstrappers. + * * @covers \Hyde\Foundation\ConsoleKernel + * + * Our custom bootstrapping system depends on code from Laravel Zero which is marked as internal. + * Sadly, there is no way around working with this private API. Since they may change the API + * at any time, we have tests here to detect if their code changes, so we can catch it early. */ class ConsoleKernelTest extends TestCase { @@ -25,28 +32,40 @@ public function testClassImplementsKernelInterface() $this->assertInstanceOf(Kernel::class, app(ConsoleKernel::class)); } - public function testBootstrappers() + public function testLaravelZeroBootstrappersHaveNotChanged() { - $kernel = app(ConsoleKernel::class); + $this->assertSame([ + \LaravelZero\Framework\Bootstrap\CoreBindings::class, + \LaravelZero\Framework\Bootstrap\LoadEnvironmentVariables::class, + \LaravelZero\Framework\Bootstrap\LoadConfiguration::class, + \Illuminate\Foundation\Bootstrap\HandleExceptions::class, + \LaravelZero\Framework\Bootstrap\RegisterFacades::class, + \LaravelZero\Framework\Bootstrap\RegisterProviders::class, + \Illuminate\Foundation\Bootstrap\BootProviders::class, + ], $this->getBootstrappersFromKernel(app(LaravelZeroKernel::class))); + } - // Normally, protected code does not need to be unit tested, but since this array is so vital, we want to inspect it. - $bootstrappers = (new ReflectionMethod($kernel, 'bootstrappers'))->invoke($kernel); + public function testHydeBootstrapperInjections() + { + $bootstrappers = $this->getBootstrappersFromKernel(app(ConsoleKernel::class)); $this->assertIsArray($bootstrappers); $this->assertContains(LoadYamlConfiguration::class, $bootstrappers); - // Another assertion that is usually a no-no, testing vendor code, especially those which are marked as internal. - // We do this here however, so we get a heads-up if the vendor code changes as that could break our code. - $this->assertSame([ - 0 => 'LaravelZero\Framework\Bootstrap\CoreBindings', - 1 => 'LaravelZero\Framework\Bootstrap\LoadEnvironmentVariables', - 2 => 'Hyde\Foundation\Internal\LoadConfiguration', - 3 => 'Illuminate\Foundation\Bootstrap\HandleExceptions', - 4 => 'LaravelZero\Framework\Bootstrap\RegisterFacades', - 5 => 'Hyde\Foundation\Internal\LoadYamlConfiguration', - 6 => 'LaravelZero\Framework\Bootstrap\RegisterProviders', - 7 => 'Illuminate\Foundation\Bootstrap\BootProviders', + \LaravelZero\Framework\Bootstrap\CoreBindings::class, + \LaravelZero\Framework\Bootstrap\LoadEnvironmentVariables::class, + \Hyde\Foundation\Internal\LoadConfiguration::class, + \Illuminate\Foundation\Bootstrap\HandleExceptions::class, + \LaravelZero\Framework\Bootstrap\RegisterFacades::class, + \Hyde\Foundation\Internal\LoadYamlConfiguration::class, + \LaravelZero\Framework\Bootstrap\RegisterProviders::class, + \Illuminate\Foundation\Bootstrap\BootProviders::class, ], $bootstrappers); } + + protected function getBootstrappersFromKernel(\Illuminate\Foundation\Console\Kernel $kernel): array + { + return (new ReflectionMethod($kernel, 'bootstrappers'))->invoke($kernel); + } } From d2d2dd88b0af7178ecafa1fad2c9b60e49b4100c Mon Sep 17 00:00:00 2001 From: github-actions Date: Wed, 3 Jul 2024 17:22:13 +0000 Subject: [PATCH 6/8] Merge pull request #1773 from hydephp/improve-yaml-environment-configuration-loading Refactor bootstrappers and improve Yaml environment configuration loading https://github.com/hydephp/develop/commit/36859cd78dc1363333047526b6206b263fff30ee --- src/Foundation/ConsoleKernel.php | 5 + .../Internal/LoadYamlConfiguration.php | 104 +---- .../Internal/LoadYamlEnvironmentVariables.php | 61 +++ .../Internal/YamlConfigurationRepository.php | 70 +++ tests/Feature/ConsoleKernelTest.php | 1 + .../HighLevelYamlConfigurationFeatureTest.php | 85 ++++ tests/Feature/LoadYamlConfigurationTest.php | 363 --------------- .../Feature/YamlConfigurationFeatureTest.php | 418 ++++++++++++++++++ 8 files changed, 655 insertions(+), 452 deletions(-) create mode 100644 src/Foundation/Internal/LoadYamlEnvironmentVariables.php create mode 100644 src/Foundation/Internal/YamlConfigurationRepository.php create mode 100644 tests/Feature/HighLevelYamlConfigurationFeatureTest.php delete mode 100644 tests/Feature/LoadYamlConfigurationTest.php create mode 100644 tests/Feature/YamlConfigurationFeatureTest.php diff --git a/src/Foundation/ConsoleKernel.php b/src/Foundation/ConsoleKernel.php index a4fee7af..f38c74ec 100644 --- a/src/Foundation/ConsoleKernel.php +++ b/src/Foundation/ConsoleKernel.php @@ -18,9 +18,14 @@ protected function bootstrappers(): array // We do this by swapping out the LoadConfiguration class with our own. // We also inject our Yaml configuration loading bootstrapper. + // First, we need to register our Yaml configuration repository, + // as this code executes before service providers are registered. + $this->app->singleton(Internal\YamlConfigurationRepository::class); + return [ \LaravelZero\Framework\Bootstrap\CoreBindings::class, \LaravelZero\Framework\Bootstrap\LoadEnvironmentVariables::class, + \Hyde\Foundation\Internal\LoadYamlEnvironmentVariables::class, \Hyde\Foundation\Internal\LoadConfiguration::class, \Illuminate\Foundation\Bootstrap\HandleExceptions::class, \LaravelZero\Framework\Bootstrap\RegisterFacades::class, diff --git a/src/Foundation/Internal/LoadYamlConfiguration.php b/src/Foundation/Internal/LoadYamlConfiguration.php index 77b4b439..d5d75acc 100644 --- a/src/Foundation/Internal/LoadYamlConfiguration.php +++ b/src/Foundation/Internal/LoadYamlConfiguration.php @@ -4,125 +4,51 @@ namespace Hyde\Foundation\Internal; -use Hyde\Hyde; -use Hyde\Facades\Config; use Illuminate\Support\Arr; -use Symfony\Component\Yaml\Yaml; +use Hyde\Foundation\Application; +use Illuminate\Config\Repository; -use function array_key_first; -use function file_get_contents; use function array_merge; -use function file_exists; /** * @internal Bootstrap service that loads the YAML configuration file. * - * @implements \LaravelZero\Framework\Contracts\BoostrapperContract [sic] - * * @see docs/digging-deeper/customization.md#yaml-configuration * * It also supports loading multiple configuration namespaces, where a configuration namespace is defined * as a firs level entry in the service container configuration repository array, and corresponds * one-to-one with a file in the config directory, and a root-level key in the YAML file. * - * This feature, by design, requires a top-level configuration entry to be present as 'hyde' in the YAML file. + * The namespace feature by design, requires a top-level configuration entry to be present as 'hyde' in the YAML file. * Existing config files will be parsed as normal, but can be migrated by indenting all entries by one level, * and adding a top-level 'hyde' key. Then additional namespaces can be added underneath as needed. */ class LoadYamlConfiguration { + protected YamlConfigurationRepository $yaml; protected array $config; - protected array $yaml; - /** - * Performs a core task that needs to be performed on - * early stages of the framework. - */ - public function bootstrap(): void + public function bootstrap(Application $app): void { - if ($this->hasYamlConfigFile()) { - $this->config = Config::all(); - $this->yaml = $this->parseYamlFile(); - - $this->supportSettingSidebarHeaderFromSiteName(); - $this->supportSettingRssFeedTitleFromSiteName(); + $this->yaml = $app->make(YamlConfigurationRepository::class); - $this->mergeParsedConfiguration(); - - Config::set($this->config); + if ($this->yaml->hasYamlConfigFile()) { + tap($app->make('config'), function (Repository $config): void { + $this->config = $config->all(); + $this->mergeParsedConfiguration(); + })->set($this->config); } } - protected function hasYamlConfigFile(): bool - { - return file_exists(Hyde::path('hyde.yml')) - || file_exists(Hyde::path('hyde.yaml')); - } - - /** @return array */ - protected function parseYamlFile(): array - { - return Arr::undot((array) Yaml::parse(file_get_contents($this->getFile()))); - } - - protected function getFile(): string - { - return file_exists(Hyde::path('hyde.yml')) - ? Hyde::path('hyde.yml') - : Hyde::path('hyde.yaml'); - } - protected function mergeParsedConfiguration(): void { - $yaml = $this->yaml; - - // If the Yaml file contains namespaces, we merge those using more granular logic - // that only applies the namespace data to each configuration namespace. - if ($this->configurationContainsNamespaces()) { - /** @var array> $yaml */ - foreach ($yaml as $namespace => $data) { - $this->mergeConfiguration($namespace, Arr::undot((array) $data)); - } - } else { - // Otherwise, we can merge using the default strategy, which is simply applying all the data to the hyde namespace. - $this->mergeConfiguration('hyde', $yaml); - } - } - - protected function mergeConfiguration(string $namespace, array $yamlData): void - { - $this->config[$namespace] = array_merge( - $this->config[$namespace] ?? [], - $yamlData - ); - } - - protected function configurationContainsNamespaces(): bool - { - return array_key_first($this->yaml) === 'hyde'; - } - - private function supportSettingSidebarHeaderFromSiteName(): void - { - $sidebarHeaderIsNotSetInPhpConfig = ($this->config['docs']['sidebar']['header'] ?? null) === 'HydePHP Docs'; - $siteNameFromYaml = $this->configurationContainsNamespaces() ? ($this->yaml['hyde']['name'] ?? null) : ($this->yaml['name'] ?? null); - - if ($sidebarHeaderIsNotSetInPhpConfig) { - if ($siteNameFromYaml !== null) { - $this->config['docs']['sidebar']['header'] = $siteNameFromYaml.' Docs'; - } + foreach ($this->yaml->getData() as $namespace => $data) { + $this->mergeConfiguration($namespace, Arr::undot($data ?: [])); } } - private function supportSettingRssFeedTitleFromSiteName(): void + protected function mergeConfiguration(string $namespace, array $yaml): void { - $rssFeedTitleIsNotSetInPhpConfig = ($this->config['hyde']['rss']['description'] ?? null) === 'HydePHP RSS Feed'; - $siteNameFromYaml = $this->configurationContainsNamespaces() ? ($this->yaml['hyde']['name'] ?? null) : ($this->yaml['name'] ?? null); - - if ($rssFeedTitleIsNotSetInPhpConfig) { - if ($siteNameFromYaml !== null) { - $this->config['hyde']['rss']['description'] = $siteNameFromYaml.' RSS Feed'; - } - } + $this->config[$namespace] = array_merge($this->config[$namespace] ?? [], $yaml); } } diff --git a/src/Foundation/Internal/LoadYamlEnvironmentVariables.php b/src/Foundation/Internal/LoadYamlEnvironmentVariables.php new file mode 100644 index 00000000..bd1f3c5a --- /dev/null +++ b/src/Foundation/Internal/LoadYamlEnvironmentVariables.php @@ -0,0 +1,61 @@ +yaml = $app->make(YamlConfigurationRepository::class); + + if ($this->yaml->hasYamlConfigFile()) { + $this->injectEnvironmentVariables(); + } + } + + protected function injectEnvironmentVariables(): void + { + if ($this->canInjectSiteNameEnvironmentVariable()) { + $this->injectSiteNameEnvironmentVariable(); + } + } + + protected function canInjectSiteNameEnvironmentVariable(): bool + { + return $this->yamlHasSiteNameSet() && ! $this->alreadyHasEnvironmentVariable(); + } + + protected function alreadyHasEnvironmentVariable(): bool + { + return filled(Env::get('SITE_NAME')); + } + + protected function injectSiteNameEnvironmentVariable(): void + { + $name = $this->getSiteNameFromYaml(); + + Env::getRepository()->set('SITE_NAME', $name); + } + + protected function yamlHasSiteNameSet(): bool + { + return isset($this->yaml->getData()['hyde']['name']); + } + + protected function getSiteNameFromYaml(): string + { + return $this->yaml->getData()['hyde']['name']; + } +} diff --git a/src/Foundation/Internal/YamlConfigurationRepository.php b/src/Foundation/Internal/YamlConfigurationRepository.php new file mode 100644 index 00000000..8745e40a --- /dev/null +++ b/src/Foundation/Internal/YamlConfigurationRepository.php @@ -0,0 +1,70 @@ +file = $this->getFilePath(); + + if ($this->hasYamlConfigFile()) { + $data = $this->parseYamlFile(); + + if (! self::configurationContainsNamespaces($data)) { + $data = ['hyde' => $data]; + } + + $this->data = $data; + } + } + + /** @return array> */ + public function getData(): array + { + return $this->data; + } + + public function hasYamlConfigFile(): bool + { + return $this->file !== false; + } + + protected function parseYamlFile(): array + { + return Arr::undot((array) Yaml::parse(file_get_contents($this->file))); + } + + protected function getFilePath(): string|false + { + return match (true) { + file_exists(Hyde::path('hyde.yml')) => Hyde::path('hyde.yml'), + file_exists(Hyde::path('hyde.yaml')) => Hyde::path('hyde.yaml'), + default => false, + }; + } + + protected static function configurationContainsNamespaces(array $config): bool + { + return array_key_first($config) === 'hyde'; + } +} diff --git a/tests/Feature/ConsoleKernelTest.php b/tests/Feature/ConsoleKernelTest.php index 6c433bdb..38d1688d 100644 --- a/tests/Feature/ConsoleKernelTest.php +++ b/tests/Feature/ConsoleKernelTest.php @@ -55,6 +55,7 @@ public function testHydeBootstrapperInjections() $this->assertSame([ \LaravelZero\Framework\Bootstrap\CoreBindings::class, \LaravelZero\Framework\Bootstrap\LoadEnvironmentVariables::class, + \Hyde\Foundation\Internal\LoadYamlEnvironmentVariables::class, \Hyde\Foundation\Internal\LoadConfiguration::class, \Illuminate\Foundation\Bootstrap\HandleExceptions::class, \LaravelZero\Framework\Bootstrap\RegisterFacades::class, diff --git a/tests/Feature/HighLevelYamlConfigurationFeatureTest.php b/tests/Feature/HighLevelYamlConfigurationFeatureTest.php new file mode 100644 index 00000000..59e28cd5 --- /dev/null +++ b/tests/Feature/HighLevelYamlConfigurationFeatureTest.php @@ -0,0 +1,85 @@ +setUpConfigurationBeforeApplicationBoots(); + + parent::setUp(); + } + + protected function tearDown(): void + { + unlink('hyde.yml'); + unlink('config/custom.php'); + + $this->clearEnvVars(); + + parent::tearDown(); + } + + protected function setUpConfigurationBeforeApplicationBoots(): void + { + file_put_contents('hyde.yml', <<<'YAML' + hyde: + name: Yaml Site Name + docs: + sidebar_order: + # Reversed compared to the default order + - getting-started + - installation + - readme + custom: + setting_one: Override + setting_two: Added + YAML); + + file_put_contents('config/custom.php', <<<'PHP' + 'Default', + 'setting_three' => 'Inherited' + ]; + PHP); + } + + public function testTestTheYamlConfigurationFeature() + { + $config = config()->all(); + + $this->assertSame('Yaml Site Name', env('SITE_NAME')); + $this->assertSame('Yaml Site Name', $_ENV['SITE_NAME']); + $this->assertSame('Yaml Site Name', $_SERVER['SITE_NAME']); + + $this->assertSame('Yaml Site Name', $config['hyde']['name']); + $this->assertSame('Yaml Site Name Docs', $config['docs']['sidebar']['header']); + $this->assertSame(['getting-started', 'installation', 'readme'], $config['docs']['sidebar_order']); + + $this->assertSame('Override', $config['custom']['setting_one']); + $this->assertSame('Added', $config['custom']['setting_two']); + $this->assertSame('Inherited', $config['custom']['setting_three']); + } + + protected function clearEnvVars(): void + { + // Todo: Can we access loader? https://github.com/vlucas/phpdotenv/pull/107/files + putenv('SITE_NAME'); + unset($_ENV['SITE_NAME'], $_SERVER['SITE_NAME']); + } +} diff --git a/tests/Feature/LoadYamlConfigurationTest.php b/tests/Feature/LoadYamlConfigurationTest.php deleted file mode 100644 index efd8ef56..00000000 --- a/tests/Feature/LoadYamlConfigurationTest.php +++ /dev/null @@ -1,363 +0,0 @@ -file('hyde.yml', <<<'YAML' - name: HydePHP - url: "http://localhost" - pretty_urls: false - generate_sitemap: true - rss: - enabled: true - filename: feed.xml - description: HydePHP RSS Feed - language: en - output_directory: _site - YAML); - $this->runBootstrapper(); - - $this->assertSame('HydePHP', Config::get('hyde.name')); - $this->assertSame('http://localhost', Config::get('hyde.url')); - $this->assertSame(false, Config::get('hyde.pretty_urls')); - $this->assertSame(true, Config::get('hyde.generate_sitemap')); - $this->assertSame(true, Config::get('hyde.rss.enabled')); - $this->assertSame('feed.xml', Config::get('hyde.rss.filename')); - $this->assertSame('HydePHP RSS Feed', Config::get('hyde.rss.description')); - $this->assertSame('en', Config::get('hyde.language')); - $this->assertSame('_site', Config::get('hyde.output_directory')); - } - - public function testCanDefineMultipleConfigSettingsInHydeYmlFile() - { - $this->file('hyde.yml', <<<'YAML' - hyde: - name: HydePHP - url: "http://localhost" - docs: - sidebar: - header: "My Docs" - YAML); - - $this->runBootstrapper(); - - $this->assertSame('HydePHP', Config::get('hyde.name')); - $this->assertSame('http://localhost', Config::get('hyde.url')); - $this->assertSame('My Docs', Config::get('docs.sidebar.header')); - } - - public function testBootstrapperAppliesYamlConfigurationWhenPresent() - { - $this->file('hyde.yml', 'name: Foo'); - $this->runBootstrapper(); - - $this->assertSame('Foo', config('hyde.name')); - } - - public function testChangesInYamlFileOverrideChangesInHydeConfig() - { - $this->file('hyde.yml', 'name: Foo'); - $this->runBootstrapper(); - - $this->assertSame('Foo', Config::get('hyde.name')); - } - - public function testChangesInYamlFileOverrideChangesInHydeConfigWhenUsingYamlExtension() - { - $this->file('hyde.yaml', 'name: Foo'); - $this->runBootstrapper(); - - $this->assertSame('Foo', Config::get('hyde.name')); - } - - public function testServiceGracefullyHandlesMissingFile() - { - $this->runBootstrapper(); - - $this->assertSame('HydePHP', Config::get('hyde.name')); - } - - public function testServiceGracefullyHandlesEmptyFile() - { - $this->file('hyde.yml', ''); - $this->runBootstrapper(); - - $this->assertSame('HydePHP', Config::get('hyde.name')); - } - - public function testCanAddArbitraryConfigKeys() - { - $this->file('hyde.yml', 'foo: bar'); - $this->runBootstrapper(); - - $this->assertSame('bar', Config::get('hyde.foo')); - } - - public function testConfigurationOptionsAreMerged() - { - config(['hyde' => [ - 'foo' => 'bar', - 'baz' => 'qux', - ]]); - - $this->file('hyde.yml', 'baz: hat'); - $this->runBootstrapper(); - - $this->assertSame('bar', Config::get('hyde.foo')); - } - - public function testCanAddConfigurationOptionsInNamespacedArray() - { - $this->file('hyde.yml', <<<'YAML' - hyde: - name: HydePHP - foo: bar - bar: - baz: qux - YAML); - - $this->runBootstrapper(); - - $this->assertSame('HydePHP', Config::get('hyde.name')); - $this->assertSame('bar', Config::get('hyde.foo')); - $this->assertSame('qux', Config::get('hyde.bar.baz')); - } - - public function testCanAddArbitraryNamespacedData() - { - $this->file('hyde.yml', <<<'YAML' - hyde: - some: thing - foo: - bar: baz - YAML); - - $this->runBootstrapper(); - - $this->assertSame('baz', Config::get('foo.bar')); - } - - public function testAdditionalNamespacesRequireTheHydeNamespaceToBePresent() - { - $this->file('hyde.yml', <<<'YAML' - foo: - bar: baz - YAML); - - $this->runBootstrapper(); - - $this->assertNull(Config::get('foo.bar')); - } - - public function testAdditionalNamespacesRequiresHydeNamespaceToBeTheFirstEntry() - { - $this->file('hyde.yml', <<<'YAML' - foo: - bar: baz - hyde: - some: thing - YAML); - - $this->runBootstrapper(); - - $this->assertNull(Config::get('foo.bar')); - } - - public function testHydeNamespaceCanBeEmpty() - { - $this->file('hyde.yml', <<<'YAML' - hyde: - foo: - bar: baz - YAML); - - $this->runBootstrapper(); - - $this->assertSame('baz', Config::get('foo.bar')); - } - - public function testHydeNamespaceCanBeNull() - { - // This is essentially the same as the empty state test above, at least according to the YAML spec. - $this->file('hyde.yml', <<<'YAML' - hyde: null - foo: - bar: baz - YAML); - - $this->runBootstrapper(); - - $this->assertSame('baz', Config::get('foo.bar')); - } - - public function testHydeNamespaceCanBlank() - { - $this->file('hyde.yml', <<<'YAML' - hyde: '' - foo: - bar: baz - YAML); - - $this->runBootstrapper(); - - $this->assertSame('baz', Config::get('foo.bar')); - } - - public function testDotNotationCanBeUsed() - { - config(['hyde' => []]); - - $this->file('hyde.yml', <<<'YAML' - foo.bar.baz: qux - YAML); - - $this->runBootstrapper(); - - $this->assertSame(['foo' => ['bar' => ['baz' => 'qux']]], Config::get('hyde')); - $this->assertSame('qux', Config::get('hyde.foo.bar.baz')); - } - - public function testDotNotationCanBeUsedWithNamespaces() - { - config(['hyde' => []]); - - $this->file('hyde.yml', <<<'YAML' - hyde: - foo.bar.baz: qux - one: - foo: - bar: - baz: qux - two: - foo.bar.baz: qux - YAML); - - $this->runBootstrapper(); - - $expected = ['foo' => ['bar' => ['baz' => 'qux']]]; - - $this->assertSame($expected, Config::get('hyde')); - $this->assertSame($expected, Config::get('one')); - $this->assertSame($expected, Config::get('two')); - } - - public function testSettingSiteNameSetsSidebarHeader() - { - $this->file('hyde.yml', <<<'YAML' - name: Example - YAML); - - $this->runBootstrapper(); - - $this->assertSame('Example Docs', Config::get('docs.sidebar.header')); - } - - public function testSettingSiteNameSetsSidebarHeaderWhenUsingHydeNamespace() - { - $this->file('hyde.yml', <<<'YAML' - hyde: - name: Example - YAML); - - $this->runBootstrapper(); - - $this->assertSame('Example Docs', Config::get('docs.sidebar.header')); - } - - public function testSettingSiteNameSetsSidebarHeaderUnlessAlreadySpecifiedInYamlConfig() - { - $this->file('hyde.yml', <<<'YAML' - hyde: - name: Example - docs: - sidebar: - header: Custom - YAML); - - $this->runBootstrapper(); - - $this->assertSame('Custom', Config::get('docs.sidebar.header')); - } - - public function testSettingSiteNameSetsSidebarHeaderUnlessAlreadySpecifiedInStandardConfig() - { - config(['docs.sidebar.header' => 'Custom']); - - $this->file('hyde.yml', <<<'YAML' - hyde: - name: Example - YAML); - - $this->runBootstrapper(); - - $this->assertSame('Custom', Config::get('docs.sidebar.header')); - } - - public function testSettingSiteNameSetsRssFeedSiteName() - { - $this->file('hyde.yml', <<<'YAML' - name: Example - YAML); - - $this->runBootstrapper(); - - $this->assertSame('Example RSS Feed', Config::get('hyde.rss.description')); - } - - public function testSettingSiteNameSetsRssFeedSiteNameWhenUsingHydeNamespace() - { - $this->file('hyde.yml', <<<'YAML' - hyde: - name: Example - YAML); - - $this->runBootstrapper(); - - $this->assertSame('Example RSS Feed', Config::get('hyde.rss.description')); - } - - public function testSettingSiteNameSetsRssFeedSiteNameUnlessAlreadySpecifiedInYamlConfig() - { - $this->file('hyde.yml', <<<'YAML' - hyde: - name: Example - rss: - description: Custom - YAML); - - $this->runBootstrapper(); - - $this->assertSame('Custom', Config::get('hyde.rss.description')); - } - - public function testSettingSiteNameSetsRssFeedSiteNameUnlessAlreadySpecifiedInStandardConfig() - { - config(['hyde.rss.description' => 'Custom']); - - $this->file('hyde.yml', <<<'YAML' - hyde: - name: Example - YAML); - - $this->runBootstrapper(); - - $this->assertSame('Custom', Config::get('hyde.rss.description')); - } - - protected function runBootstrapper(): void - { - $this->app->bootstrapWith([LoadYamlConfiguration::class]); - } -} diff --git a/tests/Feature/YamlConfigurationFeatureTest.php b/tests/Feature/YamlConfigurationFeatureTest.php new file mode 100644 index 00000000..41362764 --- /dev/null +++ b/tests/Feature/YamlConfigurationFeatureTest.php @@ -0,0 +1,418 @@ +clearEnvVars(); + + parent::tearDown(); + } + + public function testCanDefineHydeConfigSettingsInHydeYmlFile() + { + $this->file('hyde.yml', <<<'YAML' + name: Test + url: "http://localhost" + pretty_urls: false + generate_sitemap: true + rss: + enabled: true + filename: feed.xml + description: Test RSS Feed + language: en + output_directory: _site + YAML); + $this->runBootstrappers(); + + $this->assertSame('Test', config('hyde.name')); + $this->assertSame('http://localhost', config('hyde.url')); + $this->assertSame(false, config('hyde.pretty_urls')); + $this->assertSame(true, config('hyde.generate_sitemap')); + $this->assertSame(true, config('hyde.rss.enabled')); + $this->assertSame('feed.xml', config('hyde.rss.filename')); + $this->assertSame('Test RSS Feed', config('hyde.rss.description')); + $this->assertSame('en', config('hyde.language')); + $this->assertSame('_site', config('hyde.output_directory')); + } + + public function testCanDefineMultipleConfigSettingsInHydeYmlFile() + { + $this->file('hyde.yml', <<<'YAML' + hyde: + name: Test + url: "http://localhost" + docs: + sidebar: + header: "My Docs" + YAML); + + $this->runBootstrappers(); + + $this->assertSame('Test', config('hyde.name')); + $this->assertSame('http://localhost', config('hyde.url')); + $this->assertSame('My Docs', config('docs.sidebar.header')); + } + + public function testBootstrapperAppliesYamlConfigurationWhenPresent() + { + $this->file('hyde.yml', 'name: Foo'); + $this->runBootstrappers(); + + $this->assertSame('Foo', config('hyde.name')); + } + + public function testChangesInYamlFileOverrideChangesInHydeConfig() + { + $this->file('hyde.yml', 'name: Foo'); + $this->runBootstrappers(); + + $this->assertSame('Foo', config('hyde.name')); + } + + public function testChangesInYamlFileOverrideChangesInHydeConfigWhenUsingYamlExtension() + { + $this->file('hyde.yaml', 'name: Foo'); + $this->runBootstrappers(); + + $this->assertSame('Foo', config('hyde.name')); + } + + public function testServiceGracefullyHandlesMissingFile() + { + $this->runBootstrappers(); + + $this->assertSame('HydePHP', config('hyde.name')); + } + + public function testServiceGracefullyHandlesEmptyFile() + { + $this->file('hyde.yml', ''); + $this->runBootstrappers(); + + $this->assertSame('HydePHP', config('hyde.name')); + } + + public function testCanAddArbitraryConfigKeys() + { + $this->file('hyde.yml', 'foo: bar'); + $this->runBootstrappers(); + + $this->assertSame('bar', config('hyde.foo')); + } + + public function testConfigurationOptionsAreMerged() + { + $this->file('hyde.yml', 'baz: hat'); + $this->runBootstrappers(['hyde' => [ + 'foo' => 'bar', + 'baz' => 'qux', + ]]); + + $this->assertSame('bar', config('hyde.foo')); + } + + public function testCanAddConfigurationOptionsInNamespacedArray() + { + $this->file('hyde.yml', <<<'YAML' + hyde: + name: HydePHP + foo: bar + bar: + baz: qux + YAML); + + $this->runBootstrappers(); + + $this->assertSame('HydePHP', config('hyde.name')); + $this->assertSame('bar', config('hyde.foo')); + $this->assertSame('qux', config('hyde.bar.baz')); + } + + public function testCanAddArbitraryNamespacedData() + { + $this->file('hyde.yml', <<<'YAML' + hyde: + some: thing + foo: + bar: baz + YAML); + + $this->runBootstrappers(); + + $this->assertSame('baz', config('foo.bar')); + } + + public function testAdditionalNamespacesRequireTheHydeNamespaceToBePresent() + { + $this->file('hyde.yml', <<<'YAML' + foo: + bar: baz + YAML); + + $this->runBootstrappers(); + + $this->assertNull(config('foo.bar')); + } + + public function testAdditionalNamespacesRequiresHydeNamespaceToBeTheFirstEntry() + { + $this->file('hyde.yml', <<<'YAML' + foo: + bar: baz + hyde: + some: thing + YAML); + + $this->runBootstrappers(); + + $this->assertNull(config('foo.bar')); + } + + public function testHydeNamespaceCanBeEmpty() + { + $this->file('hyde.yml', <<<'YAML' + hyde: + foo: + bar: baz + YAML); + + $this->runBootstrappers(); + + $this->assertSame('baz', config('foo.bar')); + } + + public function testHydeNamespaceCanBeNull() + { + // This is essentially the same as the empty state test above, at least according to the YAML spec. + $this->file('hyde.yml', <<<'YAML' + hyde: null + foo: + bar: baz + YAML); + + $this->runBootstrappers(); + + $this->assertSame('baz', config('foo.bar')); + } + + public function testHydeNamespaceCanBlank() + { + $this->file('hyde.yml', <<<'YAML' + hyde: '' + foo: + bar: baz + YAML); + + $this->runBootstrappers(); + + $this->assertSame('baz', config('foo.bar')); + } + + public function testDotNotationCanBeUsed() + { + $this->file('hyde.yml', <<<'YAML' + foo.bar.baz: qux + YAML); + + $this->runBootstrappers(); + + $this->assertSame(['bar' => ['baz' => 'qux']], config('hyde.foo')); + $this->assertSame('qux', config('hyde.foo.bar.baz')); + } + + public function testDotNotationCanBeUsedWithNamespaces() + { + $this->file('hyde.yml', <<<'YAML' + hyde: + foo.bar.baz: qux + one: + foo: + bar: + baz: qux + two: + foo.bar.baz: qux + YAML); + + $this->runBootstrappers(); + + $expected = ['bar' => ['baz' => 'qux']]; + + $this->assertSame($expected, config('hyde.foo')); + $this->assertSame($expected, config('one.foo')); + $this->assertSame($expected, config('two.foo')); + } + + public function testSettingSiteNameSetsEnvVars() + { + $this->assertSame('HydePHP', config('hyde.name')); + + // Assert that the environment variables are not set. + $this->assertSame([ + 'env' => null, + 'Env::get' => null, + 'getenv' => false, + '$_ENV' => null, + '$_SERVER' => null, + ], $this->envVars()); + + $this->file('hyde.yml', <<<'YAML' + name: Environment Example + YAML); + + $this->runBootstrappers(); + + // Assert that the environment variables are set. + $this->assertSame([ + 'env' => 'Environment Example', + 'Env::get' => 'Environment Example', + 'getenv' => 'Environment Example', + '$_ENV' => 'Environment Example', + '$_SERVER' => 'Environment Example', + ], $this->envVars()); + + $this->assertSame('Environment Example', config('hyde.name')); + } + + public function testSettingSiteNameSetsSidebarHeader() + { + $this->file('hyde.yml', <<<'YAML' + name: Root Example + YAML); + + $this->runBootstrappers(); + + $this->assertSame('Root Example Docs', config('docs.sidebar.header')); + } + + public function testSettingSiteNameSetsSidebarHeaderWhenUsingHydeNamespace() + { + $this->file('hyde.yml', <<<'YAML' + hyde: + name: Hyde Example + YAML); + + $this->runBootstrappers(); + + $this->assertSame('Hyde Example Docs', config('docs.sidebar.header')); + } + + public function testSettingSiteNameSetsSidebarHeaderUnlessAlreadySpecifiedInYamlConfig() + { + $this->file('hyde.yml', <<<'YAML' + hyde: + name: Example + docs: + sidebar: + header: Custom + YAML); + + $this->runBootstrappers(); + + $this->assertSame('Custom', config('docs.sidebar.header')); + } + + public function testSettingSiteNameSetsSidebarHeaderUnlessAlreadySpecifiedInStandardConfig() + { + $this->file('hyde.yml', <<<'YAML' + hyde: + name: Example + YAML); + + $this->runBootstrappers(['docs.sidebar.header' => 'Custom']); + + $this->assertSame('Custom', config('docs.sidebar.header')); + } + + public function testSettingSiteNameSetsRssFeedSiteName() + { + $this->file('hyde.yml', <<<'YAML' + name: Example + YAML); + + $this->runBootstrappers(); + + $this->assertSame('Example RSS Feed', config('hyde.rss.description')); + } + + public function testSettingSiteNameSetsRssFeedSiteNameWhenUsingHydeNamespace() + { + $this->file('hyde.yml', <<<'YAML' + hyde: + name: Example + YAML); + + $this->runBootstrappers(); + + $this->assertSame('Example RSS Feed', config('hyde.rss.description')); + } + + public function testSettingSiteNameSetsRssFeedSiteNameUnlessAlreadySpecifiedInYamlConfig() + { + $this->file('hyde.yml', <<<'YAML' + hyde: + name: Example + rss: + description: Custom + YAML); + + $this->runBootstrappers(); + + $this->assertSame('Custom', config('hyde.rss.description')); + } + + public function testSettingSiteNameSetsRssFeedSiteNameUnlessAlreadySpecifiedInStandardConfig() + { + $this->file('hyde.yml', <<<'YAML' + hyde: + name: Example + YAML); + + $this->runBootstrappers(['hyde.rss.description' => 'Custom']); + + $this->assertSame('Custom', config('hyde.rss.description')); + } + + protected function runBootstrappers(?array $withMergedConfig = null): void + { + $this->refreshApplication(); + + if ($withMergedConfig !== null) { + $this->app['config']->set($withMergedConfig); + } + } + + protected function clearEnvVars(): void + { + // Todo: Can we access loader? https://github.com/vlucas/phpdotenv/pull/107/files + putenv('SITE_NAME'); + unset($_ENV['SITE_NAME'], $_SERVER['SITE_NAME']); + } + + protected function envVars(): array + { + return [ + 'env' => env('SITE_NAME'), + 'Env::get' => Env::get('SITE_NAME'), + 'getenv' => getenv('SITE_NAME'), + '$_ENV' => $_ENV['SITE_NAME'] ?? null, + '$_SERVER' => $_SERVER['SITE_NAME'] ?? null, + ]; + } +} From a9ec62aa110c69dfe15e5dfdd236cd2d7e11f87d Mon Sep 17 00:00:00 2001 From: github-actions Date: Wed, 3 Jul 2024 18:43:37 +0000 Subject: [PATCH 7/8] Merge pull request #1777 from hydephp/test-code-cleanup Internal: Clean up and refactor test code https://github.com/hydephp/develop/commit/04080323a9621be23b667b8d0947af8a2c4de3a0 --- .../Feature/Commands/MakePostCommandTest.php | 72 +++++++------- tests/Feature/PostsAuthorIntegrationTest.php | 98 +++++-------------- ...aticSiteBuilderDocumentationModuleTest.php | 22 ++--- .../StaticSiteBuilderPostModuleTest.php | 20 ++-- tests/Unit/GetLatestMarkdownPostsTest.php | 9 +- 5 files changed, 88 insertions(+), 133 deletions(-) diff --git a/tests/Feature/Commands/MakePostCommandTest.php b/tests/Feature/Commands/MakePostCommandTest.php index 86dee067..e94b3a53 100644 --- a/tests/Feature/Commands/MakePostCommandTest.php +++ b/tests/Feature/Commands/MakePostCommandTest.php @@ -4,9 +4,9 @@ namespace Hyde\Framework\Testing\Feature\Commands; -use Hyde\Facades\Filesystem; use Hyde\Hyde; use Hyde\Testing\TestCase; +use Illuminate\Support\Carbon; /** * @covers \Hyde\Console\Commands\MakePostCommand @@ -18,106 +18,110 @@ public function testCommandHasExpectedOutputAndCreatesValidFile() { // Assert that no old file exists which would cause issues $this->assertFileDoesNotExist(Hyde::path('_posts/test-post.md')); + $this->cleanUpWhenDone('_posts/test-post.md'); + + Carbon::setTestNow(Carbon::create(2024, hour: 12)); $this->artisan('make:post') ->expectsQuestion('What is the title of the post?', 'Test Post') ->expectsQuestion('Write a short post excerpt/description', 'A short description') - ->expectsQuestion('What is your (the author\'s) name?', 'PHPUnit') + ->expectsQuestion('What is your (the author\'s) name?', 'Mr Hyde') ->expectsQuestion('What is the primary category of the post?', 'general') ->expectsOutput('Creating a post with the following details:') - ->expectsOutput('Title: Test Post') ->expectsOutput('Description: A short description') ->expectsOutput('Category: general') - ->expectsOutput('Author: PHPUnit') - ->expectsOutputToContain('Date: '.date('Y-m-d')) // Don't check min/sec to avoid flaky tests + ->expectsOutput('Author: Mr Hyde') + ->expectsOutput('Date: 2024-01-01 12:00') ->expectsOutput('Identifier: test-post') - ->expectsConfirmation('Do you wish to continue?', 'yes') - ->assertExitCode(0); $this->assertFileExists(Hyde::path('_posts/test-post.md')); - $this->assertStringContainsString( - "title: 'Test Post'", - file_get_contents(Hyde::path('_posts/test-post.md')) - ); - Filesystem::unlink('_posts/test-post.md'); + $this->assertFileEqualsString(<<<'MARKDOWN' + --- + title: 'Test Post' + description: 'A short description' + category: general + author: 'Mr Hyde' + date: '2024-01-01 12:00' + --- + + ## Write something awesome. + + MARKDOWN, + Hyde::path('_posts/test-post.md') + ); } public function testThatFilesAreNotOverwrittenWhenForceFlagIsNotSet() { - file_put_contents(Hyde::path('_posts/test-post.md'), 'This should not be overwritten'); + $this->file('_posts/test-post.md', 'This should not be overwritten'); + $this->artisan('make:post') ->expectsQuestion('What is the title of the post?', 'Test Post') ->expectsQuestion('Write a short post excerpt/description', 'A short description') - ->expectsQuestion('What is your (the author\'s) name?', 'PHPUnit') + ->expectsQuestion('What is your (the author\'s) name?', 'Mr Hyde') ->expectsQuestion('What is the primary category of the post?', 'general') ->expectsOutput('Creating a post with the following details:') - ->expectsConfirmation('Do you wish to continue?', 'yes') ->expectsOutput('If you want to overwrite the file supply the --force flag.') - ->assertExitCode(409); $this->assertStringContainsString( 'This should not be overwritten', file_get_contents(Hyde::path('_posts/test-post.md')) ); - - Filesystem::unlink('_posts/test-post.md'); } public function testThatFilesAreOverwrittenWhenForceFlagIsSet() { - file_put_contents(Hyde::path('_posts/test-post.md'), 'This should be overwritten'); + $this->file('_posts/test-post.md', 'This should be overwritten'); + $this->artisan('make:post --force') ->expectsQuestion('What is the title of the post?', 'Test Post') ->expectsQuestion('Write a short post excerpt/description', 'A short description') - ->expectsQuestion('What is your (the author\'s) name?', 'PHPUnit') + ->expectsQuestion('What is your (the author\'s) name?', 'Mr Hyde') ->expectsQuestion('What is the primary category of the post?', 'general') ->expectsOutput('Creating a post with the following details:') ->expectsConfirmation('Do you wish to continue?', 'yes') - ->assertExitCode(0); $this->assertStringNotContainsString( 'This should be overwritten', file_get_contents(Hyde::path('_posts/test-post.md')) ); + $this->assertStringContainsString( "title: 'Test Post'", file_get_contents(Hyde::path('_posts/test-post.md')) ); - - Filesystem::unlink('_posts/test-post.md'); } public function testThatTitleCanBeSpecifiedInCommandSignature() { + $this->cleanUpWhenDone('_posts/test-post.md'); + $this->artisan('make:post "Test Post"') ->expectsOutputToContain('Selected title: Test Post') ->expectsQuestion('Write a short post excerpt/description', 'A short description') - ->expectsQuestion('What is your (the author\'s) name?', 'PHPUnit') + ->expectsQuestion('What is your (the author\'s) name?', 'Mr Hyde') ->expectsQuestion('What is the primary category of the post?', 'general') ->expectsConfirmation('Do you wish to continue?', 'yes') - ->assertExitCode(0); - - Filesystem::unlink('_posts/test-post.md'); } public function testThatCommandCanBeCanceled() { $this->artisan('make:post "Test Post"') - ->expectsOutputToContain('Selected title: Test Post') - ->expectsQuestion('Write a short post excerpt/description', 'A short description') - ->expectsQuestion('What is your (the author\'s) name?', 'PHPUnit') - ->expectsQuestion('What is the primary category of the post?', 'general') - ->expectsConfirmation('Do you wish to continue?') - ->expectsOutput('Aborting.') - ->assertExitCode(130); + ->expectsOutputToContain('Selected title: Test Post') + ->expectsQuestion('Write a short post excerpt/description', 'A short description') + ->expectsQuestion('What is your (the author\'s) name?', 'Mr Hyde') + ->expectsQuestion('What is the primary category of the post?', 'general') + ->expectsConfirmation('Do you wish to continue?') + ->expectsOutput('Aborting.') + ->assertExitCode(130); $this->assertFileDoesNotExist(Hyde::path('_posts/test-post.md')); } diff --git a/tests/Feature/PostsAuthorIntegrationTest.php b/tests/Feature/PostsAuthorIntegrationTest.php index 9ecfc060..a8e1a134 100644 --- a/tests/Feature/PostsAuthorIntegrationTest.php +++ b/tests/Feature/PostsAuthorIntegrationTest.php @@ -5,15 +5,13 @@ namespace Hyde\Framework\Testing\Feature; use Hyde\Facades\Author; -use Hyde\Facades\Filesystem; use Hyde\Framework\Actions\CreatesNewMarkdownPostFile; use Hyde\Hyde; use Hyde\Testing\TestCase; use Illuminate\Support\Facades\Config; /** - * Test that the Author feature works in - * conjunction with the static Post generator. + * Test that the Author feature works in conjunction with the static Post generator. * * @see StaticSiteBuilderPostModuleTest */ @@ -27,40 +25,22 @@ protected function setUp(): void } /** - * Baseline test to create a post without a defined author, - * and assert that the username is displayed as is. + * Baseline test to create a post without a defined author, and assert that the username is displayed as is. * - * Check that the author was not defined. - * We do this by building the static site and inspecting the DOM. + * Checks that the author was not defined, we do this by building the static site and inspecting the DOM. */ public function testCreatePostWithUndefinedAuthor() { - // Create a new post - (new CreatesNewMarkdownPostFile( - title: 'test-2dcbb2c-post-with-undefined-author', - description: '', - category: '', - author: 'test_undefined_author' - ))->save(true); + $this->createPostFile('post-with-undefined-author', 'test_undefined_author'); - // Check that the post was created - $this->assertFileExists(Hyde::path('_posts/test-2dcbb2c-post-with-undefined-author.md')); - - // Build the static page - $this->artisan('rebuild _posts/test-2dcbb2c-post-with-undefined-author.md')->assertExitCode(0); - - // Check that the file was created - $this->assertFileExists(Hyde::path('_site/posts/test-2dcbb2c-post-with-undefined-author.html')); + $this->artisan('rebuild _posts/post-with-undefined-author.md')->assertExitCode(0); + $this->assertFileExists(Hyde::path('_site/posts/post-with-undefined-author.html')); // Check that the author is rendered as is in the DOM $this->assertStringContainsString( '>test_undefined_author', - file_get_contents(Hyde::path('_site/posts/test-2dcbb2c-post-with-undefined-author.html')) + file_get_contents(Hyde::path('_site/posts/post-with-undefined-author.html')) ); - - // Remove the test files - Filesystem::unlink('_posts/test-2dcbb2c-post-with-undefined-author.md'); - Filesystem::unlink('_site/posts/test-2dcbb2c-post-with-undefined-author.html'); } /** @@ -68,37 +48,20 @@ public function testCreatePostWithUndefinedAuthor() */ public function testCreatePostWithDefinedAuthorWithName() { - // Create a new post - (new CreatesNewMarkdownPostFile( - title: 'test-2dcbb2c-post-with-defined-author-with-name', - description: '', - category: '', - author: 'test_named_author' - ))->save(true); - - // Check that the post was created - $this->assertFileExists(Hyde::path('_posts/test-2dcbb2c-post-with-defined-author-with-name.md')); + $this->createPostFile('post-with-defined-author-with-name', 'named_author'); Config::set('hyde.authors', [ - Author::create('test_named_author', 'Test Author', null), + Author::create('named_author', 'Test Author', null), ]); - // Check that the post was created - $this->assertFileExists(Hyde::path('_posts/test-2dcbb2c-post-with-defined-author-with-name.md')); - // Build the static page - $this->artisan('rebuild _posts/test-2dcbb2c-post-with-defined-author-with-name.md')->assertExitCode(0); - // Check that the file was created - $this->assertFileExists(Hyde::path('_site/posts/test-2dcbb2c-post-with-defined-author-with-name.html')); + $this->artisan('rebuild _posts/post-with-defined-author-with-name.md')->assertExitCode(0); + $this->assertFileExists(Hyde::path('_site/posts/post-with-defined-author-with-name.html')); // Check that the author is contains the set name in the DOM $this->assertStringContainsString( - 'Test Author', - file_get_contents(Hyde::path('_site/posts/test-2dcbb2c-post-with-defined-author-with-name.html')) + 'Test Author', + file_get_contents(Hyde::path('_site/posts/post-with-defined-author-with-name.html')) ); - - // Remove the test files - Filesystem::unlink('_posts/test-2dcbb2c-post-with-defined-author-with-name.md'); - Filesystem::unlink('_site/posts/test-2dcbb2c-post-with-defined-author-with-name.html'); } /** @@ -106,42 +69,35 @@ public function testCreatePostWithDefinedAuthorWithName() */ public function testCreatePostWithDefinedAuthorWithWebsite() { - // Create a new post - (new CreatesNewMarkdownPostFile( - title: 'test-2dcbb2c-post-with-defined-author-with-name', - description: '', - category: '', - author: 'test_author_with_website' - ))->save(true); - - // Check that the post was created - $this->assertFileExists(Hyde::path('_posts/test-2dcbb2c-post-with-defined-author-with-name.md')); + $this->createPostFile('post-with-defined-author-with-name', 'test_author_with_website'); Config::set('hyde.authors', [ Author::create('test_author_with_website', 'Test Author', 'https://example.org'), ]); - // Check that the post was created - $this->assertFileExists(Hyde::path('_posts/test-2dcbb2c-post-with-defined-author-with-name.md')); - // Build the static page - $this->artisan('rebuild _posts/test-2dcbb2c-post-with-defined-author-with-name.md')->assertExitCode(0); - // Check that the file was created - $this->assertFileExists(Hyde::path('_site/posts/test-2dcbb2c-post-with-defined-author-with-name.html')); + $this->artisan('rebuild _posts/post-with-defined-author-with-name.md')->assertExitCode(0); + $this->assertFileExists(Hyde::path('_site/posts/post-with-defined-author-with-name.html')); // Check that the author is contains the set name in the DOM $this->assertStringContainsString( 'Test Author', - file_get_contents(Hyde::path('_site/posts/test-2dcbb2c-post-with-defined-author-with-name.html')) + file_get_contents(Hyde::path('_site/posts/post-with-defined-author-with-name.html')) ); // Check that the author is contains the set website in the DOM $this->assertStringContainsString( '