composer require webchemistry/stimulus
extensions:
- WebChemistry\Stimulus\DI\StimulusExtension
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);
}
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
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[]
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>" }
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;
}
}
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(...);
<div n:stimulus="
App\Stimulus\Controller\MyController::construct('string', 'activeClass'),
App\Stimulus\Controller\MyController::switchAction()->event('click'),
"></div>
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('...'));
}
}