Skip to content

Commit

Permalink
Allow use widgets without widget factory initialization + Improve CI (#…
Browse files Browse the repository at this point in the history
…99)

* Allow use widgets without widget factory initialization

* improve CI

* Update CHANGELOG.md

Co-authored-by: Alexey Rogachev <arogachev90@gmail.com>

* Add exception

* Apply fixes from StyleCI

* fix

* improve message

* improve message

* Apply fixes from StyleCI

* improve

---------

Co-authored-by: Alexey Rogachev <arogachev90@gmail.com>
Co-authored-by: StyleCI Bot <bot@styleci.io>
  • Loading branch information
3 people committed Dec 5, 2023
1 parent c1edad7 commit c0372ef
Show file tree
Hide file tree
Showing 12 changed files with 199 additions and 27 deletions.
3 changes: 2 additions & 1 deletion .github/workflows/build.yml
Expand Up @@ -10,6 +10,7 @@ on:
- 'psalm.xml'

push:
branches: ['master']
paths-ignore:
- 'docs/**'
- 'README.md'
Expand All @@ -28,4 +29,4 @@ jobs:
os: >-
['ubuntu-latest', 'windows-latest']
php: >-
['8.0', '8.1']
['8.0', '8.1', '8.2', '8.3']
3 changes: 2 additions & 1 deletion .github/workflows/composer-require-checker.yml
Expand Up @@ -11,6 +11,7 @@ on:
- 'psalm.xml'

push:
branches: ['master']
paths-ignore:
- 'docs/**'
- 'README.md'
Expand All @@ -30,4 +31,4 @@ jobs:
os: >-
['ubuntu-latest']
php: >-
['8.0']
['8.0', '8.1', '8.2', '8.3']
3 changes: 2 additions & 1 deletion .github/workflows/mutation.yml
Expand Up @@ -9,6 +9,7 @@ on:
- 'psalm.xml'

push:
branches: ['master']
paths-ignore:
- 'docs/**'
- 'README.md'
Expand All @@ -26,6 +27,6 @@ jobs:
os: >-
['ubuntu-latest']
php: >-
['8.1']
['8.2']
secrets:
STRYKER_DASHBOARD_API_KEY: ${{ secrets.STRYKER_DASHBOARD_API_KEY }}
4 changes: 3 additions & 1 deletion .github/workflows/rector.yml
Expand Up @@ -14,8 +14,10 @@ name: rector
jobs:
rector:
uses: yiisoft/actions/.github/workflows/rector.yml@master
secrets:
token: ${{ secrets.YIISOFT_GITHUB_TOKEN }}
with:
os: >-
['ubuntu-latest']
php: >-
['8.0']
['8.3']
3 changes: 2 additions & 1 deletion .github/workflows/static.yml
Expand Up @@ -10,6 +10,7 @@ on:
- 'phpunit.xml.dist'

push:
branches: ['master']
paths-ignore:
- 'docs/**'
- 'README.md'
Expand All @@ -28,4 +29,4 @@ jobs:
os: >-
['ubuntu-latest']
php: >-
['8.0', '8.1']
['8.0', '8.1', '8.2', '8.3']
2 changes: 2 additions & 0 deletions CHANGELOG.md
Expand Up @@ -3,6 +3,8 @@
## 2.2.0 under development

- Enh #98: Add ability to set default theme for concrete widget (@vjik)
- Enh #99: Allow to use widgets without widget factory initialization (@vjik)
- Chg #99: Mark `WidgetFactoryInitializationException` as deprecated (@vjik)

## 2.1.0 November 16, 2023

Expand Down
2 changes: 1 addition & 1 deletion composer.json
Expand Up @@ -30,7 +30,7 @@
"php": "^8.0",
"psr/container": "^1.0|^2.0",
"yiisoft/definitions": "^3.1",
"yiisoft/factory": "^1.0",
"yiisoft/factory": "^1.2",
"yiisoft/friendly-exception": "^1.0",
"yiisoft/html": "^2.0|^3.0"
},
Expand Down
69 changes: 69 additions & 0 deletions src/NotInstantiableException.php
@@ -0,0 +1,69 @@
<?php

declare(strict_types=1);

namespace Yiisoft\Widget;

use Throwable;
use Yiisoft\Definitions\Exception\NotInstantiableException as FactoryNotInstantiableException;
use Yiisoft\FriendlyException\FriendlyExceptionInterface;

final class NotInstantiableException extends FactoryNotInstantiableException implements FriendlyExceptionInterface
{
public function __construct(
private string $widgetClassName,
private bool $widgetFactoryInitialized,
private Throwable $previous,
) {
$message = 'Failed to create a widget "' . $this->widgetClassName . '". ' . $previous->getMessage();
if (!$this->widgetFactoryInitialized) {
$message .= ' Perhaps you need to initialize "' . WidgetFactory::class . '" with DI container to resolve dependencies.';
}

parent::__construct($message, previous: $previous);
}

public function getName(): string
{
return 'Failed to create a widget "' . $this->widgetClassName . '". ' . $this->previous->getMessage();
}

public function getSolution(): ?string
{
if ($this->widgetFactoryInitialized) {
return null;
}

$widgetFactoryClass = WidgetFactory::class;

return <<<SOLUTION
Perhaps you need to initialize "$widgetFactoryClass" with DI container to resolve dependencies.
To initialize the widget factory call `WidgetFactory::initialize()` before using the widget.
It is a good idea to do that for the whole application.
Example:
```php
/**
* @var Psr\Container\ContainerInterface \$container
*/
Yiisoft\Widget\WidgetFactory::initialize(
container: \$container,
definitions: [MyWidget::class => new MyWidget(/*...*/)],
themes: [
'custom' => [
MyWidget::class => [
'setValue()' => [42],
],
],
],
validate: true, // Whether definitions need to be validated.
);
```
See Yii example in the configuration file of this package `config/bootstrap.php`.
SOLUTION;
}
}
27 changes: 19 additions & 8 deletions src/WidgetFactory.php
Expand Up @@ -8,7 +8,7 @@
use Yiisoft\Definitions\ArrayDefinition;
use Yiisoft\Definitions\Exception\CircularReferenceException;
use Yiisoft\Definitions\Exception\InvalidConfigException;
use Yiisoft\Definitions\Exception\NotInstantiableException;
use Yiisoft\Definitions\Exception\NotInstantiableException as FactoryNotInstantiableException;
use Yiisoft\Definitions\Helpers\ArrayDefinitionHelper;
use Yiisoft\Definitions\Helpers\DefinitionValidator;
use Yiisoft\Factory\NotFoundException;
Expand All @@ -21,6 +21,7 @@
*/
final class WidgetFactory
{
private static bool $initialized = false;
private static ?Factory $factory = null;

/**
Expand All @@ -35,6 +36,9 @@ final class WidgetFactory
*/
private static array $widgetDefaultThemes = [];

/**
* @codeCoverageIgnore
*/
private function __construct()
{
}
Expand All @@ -49,7 +53,7 @@ private function __construct()
* @see Factory::__construct()
*/
public static function initialize(
ContainerInterface $container,
?ContainerInterface $container = null,
array $definitions = [],
bool $validate = true,
array $themes = [],
Expand All @@ -66,6 +70,8 @@ public static function initialize(
self::$themes = $themes;
self::$defaultTheme = $defaultTheme;
self::$widgetDefaultThemes = $widgetDefaultThemes;

self::$initialized = true;
}

public static function setDefaultTheme(?string $theme): void
Expand All @@ -79,11 +85,10 @@ public static function setDefaultTheme(?string $theme): void
* @param array $config The parameters for creating a widget.
* @param string|null $theme The widget theme.
*
* @throws WidgetFactoryInitializationException If factory was not initialized.
* @throws CircularReferenceException
* @throws InvalidConfigException
* @throws NotFoundException
* @throws NotInstantiableException
* @throws FactoryNotInstantiableException
*
* @see Factory::create()
*
Expand All @@ -93,9 +98,7 @@ public static function setDefaultTheme(?string $theme): void
public static function createWidget(array $config, ?string $theme = null): Widget
{
if (self::$factory === null) {
throw new WidgetFactoryInitializationException(
'Widget factory should be initialized with WidgetFactory::initialize() call.',
);
self::$factory = new Factory();
}

$className = $config[ArrayDefinition::CLASS_NAME] ?? null;
Expand All @@ -109,7 +112,15 @@ public static function createWidget(array $config, ?string $theme = null): Widge
}
}

return self::$factory->create($config);
try {
return self::$factory->create($config);
} catch (FactoryNotInstantiableException $exception) {
/**
* @var string $className When `$className` is not string, `$factory->create()` does not throw
* {@see FactoryNotInstantiableException} exception.
*/
throw new NotInstantiableException($className, self::$initialized, $exception);
}
}

/**
Expand Down
3 changes: 3 additions & 0 deletions src/WidgetFactoryInitializationException.php
Expand Up @@ -7,6 +7,9 @@
use RuntimeException;
use Yiisoft\FriendlyException\FriendlyExceptionInterface;

/**
* @deprecated Will be removed in version 3.0.
*/
final class WidgetFactoryInitializationException extends RuntimeException implements FriendlyExceptionInterface
{
public function getName(): string
Expand Down
19 changes: 19 additions & 0 deletions tests/Stubs/Garage.php
@@ -0,0 +1,19 @@
<?php

declare(strict_types=1);

namespace Yiisoft\Widget\Tests\Stubs;

use Yiisoft\Widget\Widget;

final class Garage extends Widget
{
public function __construct(Car $car)
{
}

public function render(): string
{
return 'Car in garage.';
}
}
88 changes: 75 additions & 13 deletions tests/WidgetTest.php
Expand Up @@ -7,9 +7,13 @@
use PHPUnit\Framework\TestCase;
use ReflectionClass;
use RuntimeException;
use Throwable;
use Yiisoft\Definitions\Exception\InvalidConfigException;
use Yiisoft\Definitions\Exception\NotInstantiableException as FactoryNotInstantiableException;
use Yiisoft\Test\Support\Container\SimpleContainer;
use Yiisoft\Widget\NotInstantiableException;
use Yiisoft\Widget\Tests\Stubs\Car;
use Yiisoft\Widget\Tests\Stubs\Garage;
use Yiisoft\Widget\Tests\Stubs\ImmutableWidget;
use Yiisoft\Widget\Tests\Stubs\Injectable;
use Yiisoft\Widget\Tests\Stubs\TestInjectionWidget;
Expand Down Expand Up @@ -135,7 +139,7 @@ public function testStackTrackingDisorder(): void
/**
* @depends testBeginEnd
*/
public function testStackTrackingDiferentClass(): void
public function testStackTrackingDifferentClass(): void
{
$this->expectException(RuntimeException::class);
TestWidgetA::widget()->begin();
Expand All @@ -148,20 +152,62 @@ public function testInjection(): void
$this->assertInstanceOf(Injectable::class, $widget->getInjectable());
}

public function testWidgetThrownExceptionForNotInitializeWidgetFactory(): void
public function testInjectionWithoutInitialization(): void
{
$widgetFactoryReflection = new ReflectionClass(WidgetFactory::class);
$reflection = new ReflectionClass($widgetFactoryReflection->newInstanceWithoutConstructor());
$property = $reflection->getProperty('factory');
$property->setAccessible(true);
$property->setValue($widgetFactoryReflection, null);
$property->setAccessible(false);
$this->uninitializedWidgetFactory();

$exception = null;
try {
Garage::widget();
} catch (Throwable $exception) {
}

$this->assertInstanceOf(NotInstantiableException::class, $exception);
$this->assertInstanceOf(FactoryNotInstantiableException::class, $exception->getPrevious());
$this->assertSame(
'Failed to create a widget "' . Garage::class . '". ' .
'Can not instantiate ' . Car::class .
'. Perhaps you need to initialize "' . WidgetFactory::class . '" with DI container to resolve dependencies.',
$exception->getMessage()
);
$this->assertSame(
'Failed to create a widget "' . Garage::class . '". Can not instantiate ' . Car::class . '.',
$exception->getName()
);
$this->assertStringContainsString('`WidgetFactory::initialize()`', $exception->getSolution());
}

$this->expectException(WidgetFactoryInitializationException::class);
$this->expectExceptionMessage('Widget factory should be initialized with WidgetFactory::initialize() call.');
TestWidget::widget()
->id('w0')
->render();
public function testNotInstantiableWithInitialization(): void
{
$exception = null;
try {
Garage::widget();
} catch (Throwable $exception) {
}

$this->assertInstanceOf(NotInstantiableException::class, $exception);
$this->assertInstanceOf(FactoryNotInstantiableException::class, $exception->getPrevious());
$this->assertSame(
'Failed to create a widget "' . Garage::class . '". ' .
'Can not instantiate ' . Car::class . '.',
$exception->getMessage()
);
$this->assertSame(
'Failed to create a widget "' . Garage::class . '". Can not instantiate ' . Car::class . '.',
$exception->getName()
);
$this->assertNull($exception->getSolution());
}

public function testWithoutInitialization(): void
{
$this->uninitializedWidgetFactory();

$html = TestWidget::widget()->id('w0')->render();

$expected = '<run-w0>';

$this->assertSame($expected, $html);
}

public function testWidgetFactoryInitializationExceptionMessages(): void
Expand Down Expand Up @@ -216,4 +262,20 @@ public function testInvalidConstructorInConfig(): void
);
Car::widget(['name' => 'X'], ['__construct()' => 'red']);
}

private function uninitializedWidgetFactory(): void
{
$widgetFactoryReflection = new ReflectionClass(WidgetFactory::class);
$reflection = new ReflectionClass($widgetFactoryReflection->newInstanceWithoutConstructor());

$property = $reflection->getProperty('factory');
$property->setAccessible(true);
$property->setValue($widgetFactoryReflection, null);
$property->setAccessible(false);

$property = $reflection->getProperty('initialized');
$property->setAccessible(true);
$property->setValue($widgetFactoryReflection, false);
$property->setAccessible(false);
}
}

0 comments on commit c0372ef

Please sign in to comment.