webchemistry / stimulus
Installs: 2 798
Dependents: 0
Suggesters: 0
Security: 0
Stars: 0
Watchers: 3
Forks: 0
Open Issues: 0
Requires
- php: >=8.0
- doctrine/lexer: ^1.2
- nette/finder: ^2.5 || ^3.0
- nette/php-generator: ^3.5.0 || ^4.0.0
- typertion/php: ^1.0
- utilitte/php: ^1.5
- webchemistry/simple-json: ^1.0
Requires (Dev)
- latte/latte: ^3.0.0
- nette/application: ^3.1
- nette/di: ^3.0
- nette/forms: ^3.1
- phpstan/phpstan: ^1.8
README
composer require webchemistry/stimulus
extensions: - WebChemistry\Stimulus\DI\StimulusExtension
Set up extractor and generators
First, set up extractor and generator. Namespaced identifiers also supported.
Code with comments (copy-paste code is below):
require __DIR__ . '/vendor/autoload.php'; // directory with controllers, only *_controller.js and *-controller.js are extracted $extractor = new JavascriptSourceExtractor(__DIR__ . '/js/stimulus/controllers'); // optional, we want UI namespace instead of Ui $keywords = ['ui' => 'UI']; // generate classes as Stimulus\*\_*Controller, these classes we don't edit $originalClassNameConverter = new PrependClassNameConverter( 'Stimulus\\', new AppendClassNameConverter( new BaseClassNameConverter($keywords, fn (string $className) => '_' . $className), 'Controller', ), ); // We want edit these classes $emptyClassNameConverter = new PrependClassNameConverter( 'App\\Stimulus\\Controller\\', new AppendClassNameConverter( new BaseClassNameConverter($keywords), 'Controller', ), ); // for autocomplete_controller.js these controllers are generated // class Stimulus\_AutocompleteController // class App\Stimulus\Controller\AutocompleteController extends Stimulus\_AutocompleteController // for namespace/autocomplete_controller.js these controllers are generated // class Stimulus\Namespace\_AutocompleteController // class App\Stimulus\Controller\Namespace\AutocompleteController extends Stimulus\_AutocompleteController // Generator generates static methods $generator = new StaticClassStimulusControllerGenerator($extractor, $originalClassNameConverter); // Generator generates class with empty body and extends original class (Stimulus\*\_*Controller) $emptyGenerator = new EmptyClassStimulusControllerGenerator( $extractor, $emptyClassNameConverter, $originalClassNameConverter, ); // Files are written in app/generated/stimulus/*, Stimulus\ namespace have to be removed from path $writer = new FilesystemWriter(__DIR__ . '/app/generated/stimulus', 'Stimulus\\'); // Files are written in app/src/Stimulus/Controller/*, App\Stimulus\Controller\ namespace have to be removed from path, if file exists don't rewrite it $emptyWriter = new FilesystemWriter(__DIR__ . '/app/src/Stimulus/Controller', 'App\\Stimulus\\Controller\\', false); foreach ($generator->generate() as $generated) { $writer->write($generated); } foreach ($emptyGenerator->generate() as $generated) { $emptyWriter->write($generated); }
copy-paste code:
require __DIR__ . '/vendor/autoload.php'; $extractor = new JavascriptSourceExtractor(__DIR__ . '/js/stimulus/controllers'); $keywords = ['ui' => 'UI']; $originalClassNameConverter = new PrependClassNameConverter( 'Stimulus\\', new AppendClassNameConverter( new BaseClassNameConverter($keywords, fn (string $className) => '_' . $className), 'Controller', ), ); $emptyClassNameConverter = new PrependClassNameConverter( 'App\\Stimulus\\Controller\\', new AppendClassNameConverter( new BaseClassNameConverter($keywords), 'Controller', ), ); $generator = new StaticClassStimulusControllerGenerator($extractor, $originalClassNameConverter); $emptyGenerator = new EmptyClassStimulusControllerGenerator( $extractor, $emptyClassNameConverter, $originalClassNameConverter, ); $writer = new FilesystemWriter(__DIR__ . '/app/generated/stimulus', 'Stimulus\\'); $emptyWriter = new FilesystemWriter(__DIR__ . '/app/src/Stimulus/Controller', 'App\\Stimulus\\Controller\\', false); foreach ($generator->generate() as $generated) { $writer->write($generated); } foreach ($emptyGenerator->generate() as $generated) { $emptyWriter->write($generated); }
How files are generated
Class JavascriptSourceExtractor
uses javascript comments for generating.
Each controller must be annotated with @controller
, actions with @action
and their parameters with @param
, values, classes and targets with @property
.
my_controller.js
/** * @controller * * @property {String} stringValue * * @property {HTMLElement[]} itemTargets * @property {HTMLElement} resultsTarget * * @property {String} activeClass */ export default class extends Controller { static targets = ['results', 'item']; static values = { string: String, }; static classes = ['active']; /** * @action */ switch() { } }
This PHP class is generated:
declare(strict_types = 1); /** * NOTE: This class is auto generated by file: my_controller.js * Do not edit the class manually */ namespace Stimulus; use WebChemistry\Stimulus\Type\StimulusAction; use WebChemistry\Stimulus\Type\StimulusController; use WebChemistry\Stimulus\Type\StimulusTarget; abstract class _MyController { final public const identifier = 'my'; public static function construct(string $stringValue, string $activeClass): StimulusController { return new StimulusController(self::identifier, [ 'stringValue' => $stringValue, 'activeClass' => $activeClass, ], []); } public static function itemTarget(): StimulusTarget { return new StimulusTarget(self::identifier, 'itemTarget'); } public static function switchAction(): StimulusAction { return new StimulusAction(self::identifier, 'switch', []); } }
By default, each property is required, if we want to make optional we have several ways:
- add
?
to the end of property name:@property {String} stringValue?
- add
{ optional }
to the 3rd section (options) of property:@property {String} stringValue {optional}
- add hasser
@property {Boolean} hasStringValue
Javascript types and PHP types
webchemisty/stimulus introduces stricter environment for writting application. Nowadays, everyone use static analysis (at least they should) so correct types are crucial.
Library converts javascript types in the following way:
Number
=> int|float
narrowing is achieved by options (3rd section) { number: int }
Array
and Object
=> mixed[]
Boolean
=> bool
String
=> string
other
=> mixed
Arrays:
String[]
=> string[]
Number[]
=> array<int|float>
narrowing: { number: float }
Bool[]
=> bool[]
other
=> mixed[]
Custom types
Sometimes we need overriding comment types @param ...
and types method(... $type)
fot this there is options type
and commentType
e.g. { type: mixed, commentType: "array<string, resource>" }
Action parameters
Stimulus 3 introduced parameters for actions. For generating just use intersection type or just object type.
export default class extends Controller { /** * @action * @param { { params: { value: String } } & PointerEvent} event */ switch(event) { const { value } = event.params; } /** * @action * @param { { params: { value: String } } } event */ switchTwo(event) { const { value } = event.params; } }
Usage in PHP
use WebChemistry\Stimulus\Renderer\HtmlRenderer; $htmlAttributes = HtmlRenderer::render( App\Stimulus\Controller\MyController::construct('string', 'activeClass'), App\Stimulus\Controller\MyController::switchAction()->event('click'), ); // data-controller="my" data-my-string-value="string" data-my-active-class="active" data-action="click->my#switch" // or as array [attribute => value] HtmlRenderer::toArray(...);
Usage in Latte
<div n:stimulus=" App\Stimulus\Controller\MyController::construct('string', 'activeClass'), App\Stimulus\Controller\MyController::switchAction()->event('click'), "></div>
Controller as service
Sometimes we want to inject other services:
declare(strict_types = 1); namespace App\Stimulus\Controller; use Stimulus\_MyController as ParentController; use WebChemistry\Stimulus\Type\StimulusController; final class MyController extends ParentController { public function __construct( private LinkGenerator $linkGenerator, ) {} public function doConstruct(): StimulusController { return self::construct($this->linkGenerator->link('...')); } }