Phpactor Extensions
Last modified 2024/12/07 11:48Over the past month or so I have been gradually migrating Phpactor to use Extensions.
This started because I wanted to add Language Server capabilities to Phpactor,
but having two RPC mechanisms in the same application seemed overkill, so I
decided to extract everything into extensions in order that all of the
components could be easily reused and recombined (so that a
phpactor-language-server
standalone application could be created).
In addition I wanted the ability to add framework and tool specific functionality, which doesn’t belong in the main distribution. This all pointed the way to having user extensions.
Writing an Extension ¶
Extensions have a few key attributes:
- The extension package should have a package type of
phpactor-extension
and an extra attributephpactor.extension_class
which points to… - The extension class which implements
Phpactor\Container\Extension
.
That’s it. The extension class is just a DI container (similar to Pimple but with tags and parameters) with additional configuration (something like the Symfony Option Resolver).
Stupid Completor ¶
DISCLAIMER: Phpactor is not currently not stable, and some packages have no tagged release at all.
Lets make a completion extension. This extension will accept some
configuration: stupid_completor.items
and it will return these items as
suggestions every time it is invoked.
First of all we will need to require the phpactor/container
package (this is
the only strict requirement) and the phpactor/completion-extension
(as we
are building a completor) and ensure our composer file has the following attributes:
- A
type
ofphpactor-extension
- An
extra
property with the FQN of the extension class.
It might look something like this:
{
"name": "acme/stupid-completion-extension",
"description": "Stupid Completion Support",
"license": "MIT",
"type": "phpactor-extension",
"minimum-stability": "dev",
"require": {
"phpactor/container": "^1.0",
"phpactor/completion-extension": "~0.1",
},
"autoload": {
"psr-4": {
"Acme\\Extension\\StupidCompletion\\": "lib/"
}
},
"extra": {
"phpactor.extension_class": "Acme\\Extension\\StupidCompletion\\StupidCompletionExtension"
}
}
NOTE: that the completion extension has no release at time of writing so
minimum-stability: dev
is currently required.
We need to create a completor class to provide our stupid suggestions,
let’s put it in lib/Completion/StupidCompletion.php
:
<?php
namespace Acme\Extension\StupidCompletion\Completion;
use Generator;
use Phpactor\Completion\Core\Completor;
use Phpactor\Completion\Core\Suggestion;
class StupidCompletion implements Completor
{
private $suggestions;
public function __construct(array $suggestions)
{
$this->suggestions = $suggestions;
}
public function complete(string $source, int $byteOffset): Generator
{
foreach ($this->suggestions as $suggestion) {
yield Suggestion::create($suggestion);
}
}
}
Now we need the extension class, this will integrate our completor, this
should be in lib/StupidCompletionExtension.php
as with the above:
<?php
namespace Acme\Extension\StupidCompletion;
use Acme\Extension\StupidCompletion\Completion\StupidCompletion;
use Phpactor\Container\Container;
use Phpactor\Container\ContainerBuilder;
use Phpactor\Container\Extension;
use Phpactor\Extension\Completion\CompletionExtension;
use Phpactor\MapResolver\Resolver;
class StupidCompletionExtension implements Extension
{
public const PARAM_ITEMS = 'stupid_completor.items';
public function load(ContainerBuilder $container)
{
$container->register('stupid_completor.stupid_completor', function (Container $container) {
return new StupidCompletion(
$container->getParameter(self::PARAM_ITEMS)
);
}, [ CompletionExtension::TAG_COMPLETOR => []]);
}
public function configure(Resolver $schema)
{
$schema->setDefaults([
self::PARAM_ITEMS => [
'hello', 'goodbye'
]
]);
}
}
Note that above:
- We add a tag to our completor from the
CompletionExtension
. Anything that is “public” is exposed as a public constant, including tags and services (TAG_*
andSERVICE_*
). - We set some default configuration, when used with Phpactor this can be set
in
.phpactor.yml
asstupid_completor.items
.
Testing it Out ¶
You could probably now push your extension to packagist, or add it as a path
repository in Phpactor’s
extensions/extensions.json
file (which is actually a composer.json
file):
"repositories": [
{
"type": "path",
"url": "\/home\/daniel\/www\/phpactor\/stupid-completor-extension"
}
]
Once this is done you are ready to install it with:
$ ~/.vim/plugged/phpactor/bin/phpactor extension:install acme/stupid-completion-extension
Note that Phpactor will load extensions based on the contents of the file
extensions/extensions.php
- if you experience issues you may want to disable
the extension temporarily in this file.
Making a Standalone Application ¶
Sometimes you might create an extension which can be used standalone. This is beneficial for user testing and if the extension can be useful without Phpactor.
Our standalone application will provide completion results over Phpactor’s RPC protocol and will need the command line interface, so require the following:
$ composer require phpactor/completion-rpc-extension phpactor/console-extension
Create a standalone RPC application for stupid completion: just create the
following file in bin/stupid-completion
:
#!/usr/bin/env php
<?php
use Acme\Extension\StupidCompletion\StupidCompletionExtension;
use Phpactor\Container\PhpactorContainer;
use Phpactor\Extension\Completion\CompletionExtension;
use Phpactor\Extension\Console\ConsoleExtension;
use Phpactor\Extension\Logger\LoggingExtension;
use Phpactor\Extension\Rpc\RpcExtension;
use Phpactor\FilePathResolverExtension\FilePathResolverExtension;
use Symfony\Component\Console\Application;
require __DIR__ . '/../vendor/autoload.php';
$container = PhpactorContainer::fromExtensions([
StupidCompletionExtension::class,
CompletionExtension::class,
ConsoleExtension::class,
RpcExtension::class,
LoggingExtension::class,
FilePathResolverExtension::class,
], []);
$application = new Application();
$application->setCommandLoader(
$container->get(ConsoleExtension::SERVICE_COMMAND_LOADER)
);
$application->run();
Note that:
- We instantiate a
PhpactorContainer
- We manually added all the required extensions (the container will shout at you if any extensions were missing).
- We create a new Symfony Application and retrieve the command loader from the console extension.
- We run the application
Make it executable with chmod a+x bin/stupid-completion
and now
you have a stupid RPC completor!
$ echo '{"action": "complete", "parameters": {"source": "<?php ", "offset": 2}}' | ./bin/stupid rpc --pretty
{
"version": "1.0.0",
"action": "return",
"parameters": {
"value": {
"suggestions": [
{
"type": null,
"name": "hello",
"label": "hello",
"short_description": null,
"class_import": null,
"info": null
},
{
"type": null,
"name": "goodbye",
"label": "goodbye",
"short_description": null,
"class_import": null,
"info": null
}
],
"issues": []
}
}
}
Summary ¶
Extensions should allow Phpactor to be extended in all sorts of ways, as well as providing a very fast way to create entirely new applications based on Phpactor functionality.
The above extension ommits tests for the completor and the extension itself. For a simple(ish) working example see the behat extension.