Extending the SDK

Edit on GitHub

The SDK offers different extension points to enable third parties to contribute to the SDK without modifying it.

From simple to complex, the SDK can be extended by:

  • Providing additional tasks or settings via YAML definition placed inside <path/to/spryker/sdk>/extension/<YourBundleName>/Tasks/<taskname>.yaml. Those tasks can’t introduce additional dependencies and are best suited to integrate existing tools that come with a standalone executable.
  • Providing additional tasks, value resolvers, or settings via the PHP implementation placed inside <path/to/spryker/sdk>/extension/<YourBundleName>/Tasks/<taskname>.php. Those tasks need to implement the TaskInterface and need to be exposed by providing a Symfony bundle to the Spryker SDK, such as <path/to/spryker/sdk>/extension/<YourBundleName>/<YourBundleName>Bundle.php, following the conventions of a Symfony bundle. This approach is best suited for more complex tasks that don’t require additional dependencies, for example, validating content of a YAML file by using Symphony validators.
  • Providing additional tasks, value resolvers, or settings that come with additional dependencies. This approach follows the same guideline as the previous approach with the PHP implementation but requires building your own SDK docker image that includes those dependencies.

To extend the SDK, follow these steps.

1. Implement a new task

A task is the execution of a very specific function. For example, executing an external tool through a CLI call is a task.

There are two possibilities to define a new task: based on YAML for simple task definitions, and implementation via PHP and Symfony services for specialized purposes.

Implementation via YAML definition

YAML-based tasks need to fulfill a defined structure so you can execute them from the SDK. The command defined in the YAML definition can have placeholders that you need to define in the placeholder section. Each placeholder needs to map to one value resolver.

Add the definition for your task in <path>/Tasks/<name>.yaml:

---
id: string #e.g.: validation:code
short_description: string #e.g.: Fix code style violations
help: string|null #e.g: Fix codestyle violations, lorem ipsum, etc.
stage: string #e.g.: build
command: string #e.g.: php %project_dir%/vendor/bin/phpcs -f --standard=%project_dir%/vendor/spryker/code-sniffer/Spryker/ruleset.xml %module_dir%
type: string #e.g.: local_cli
placeholders:
- name: string #e.g.: %project_dir%
  valueResolver: string #e.g.: PROJECT_DIR, mapping to a value resolver with id PROJECT_DIR or  a FQCN
  optional: bool
Adding tasks to the SDK

You can add the tasks located in extension/<your extension name>/Tasks to the SDK by executing spryker-sdk sdk:update:all

Implementation via a PHP class

In case when a task is more than just a call to an existing tool, you can implement the task as a PHP class and register the task using the Symfony service tagging feature. This requires you to make the task a part of the Symfony bundle. To achieve this, follow these steps:

  1. Create s Symfony bundle.
    Refer to the official Symfony documentation for details on how to do that.
Info

The bundle has to use the Spryker SDK Contracts via Composer.

  1. Implement the task:
namespace <YourNamespace>\Tasks;

use SprykerSdk\Sdk\Contracts\Entity\TaskInterface;
use <YourNamespace>\Tasks\Commands\YourCommand;

class YourTask implements TaskInterface
{
    /**
     * @return string
     */
    public function getShortDescription(): string {}

    /**
     * @return array<\SprykerSdk\Sdk\Contracts\Entity\PlaceholderInterface>
     */
    public function getPlaceholders(): array {}

    /**
     * @return string|null
     */
    public function getHelp(): ?string {}


    public function getId(): string {}

    /**
     * @return array<\SprykerSdk\Sdk\Contracts\Entity\CommandInterface>
     */
    public function getCommands(): array
    {
        return [
            new YourCommand(),
        ];
    }

}
  1. Implement the command.
    While the task definition serves as a general description of the task and maps placeholders to value resolvers, a command serves as a function that is executed along with the resolved placeholders.

Implement the command as shown in the example:

namespace <YourNamespace>\Tasks\Commands;

use SprykerSdk\Sdk\Contracts\Entity\ExecutableCommandInterface;

class YourCommand implements ExecutableCommandInterface
{
    /**
     * @return string
     */
    public function getCommand(): string
    {
        return static::class;
    }

    /**
     * @return string
     */
    public function getType(): string
    {
        //use 'php' to execute command inside the SDK
        return 'php';
    }

    /**
     * @return bool
     */
    public function hasStopOnError(): bool
    {
        return true;
    }

    /**
     * @param array<string, mixed> $resolvedValues
     *
     * @return int
     */
    public function execute(array $resolvedValues): int
    {
        //your implementation of the command
        //$resolvedValues will be array<placeholder.name, value>
        //return 0 for success and any non 0 integer up to 255 for failed
        return 0;
    }
}

  1. Implement placeholders.
    Placeholders are resolved at runtime by using a specified value resolver. A placeholder needs a specific name that is not used anywhere in the commands the placeholder is used for.
Info

You can append % and suffix the placeholder, which makes the placeholder easier to recognize in a command.

You can reference the used value resolver by its ID or the fully qualified class name (FQCN), whereas the FQCN is preferred.

Implement the placeholder as shown in the example:

namespace <YourNamespace>\Tasks;

use SprykerSdk\Sdk\Core\Domain\Entity\Placeholder;
use SprykerSdk\Sdk\Contracts\Entity\TaskInterface;
use <YourNamespace>\ValueResolvers\YourValueResolver;

class YourTask implements TaskInterface
{
    /**
     * @return array<\SprykerSdk\Sdk\Contracts\Entity\PlaceholderInterface>
     */
    public function getPlaceholders(): array
    {
        return [
            new Placeholder('%some_placeholder%', YourValueResolver::class, [], false),
        ];
    }
}
  1. Implement a Symfony service.
    Once you have implemented the task, register it as a Symfony service.

Implement the service as shown in the example:

services:
  your_task:
    class: <YourNamespace>\Tasks\YourTask
    tags: ['sdk.task']
  1. Register your bundle.

If your bundle does not have dependencies that differ from the Spryker SDK, you don’t need to register the bundle. Instead, place it into the extension directory that is a part of your SDK installation.

For more complex bundles that require additional dependencies, follow the guidelines in Building a flavored Spryker SDK.

2. Add a value resolver

Most placeholders need a solution to resolve their values during runtime. This can be reading some settings and assembling a value based on the settings content, or any solution that turns a placeholder into a resolved value.

Make sure to unify value resolvers and always use the same name for a value.

Implement the value resolver as shown in the example:

namespace <YourNamespace>\ValueResolvers;

use SprykerSdk\Sdk\Contracts\ValueResolver\ValueResolverInterface;

class YourValueResolver implements ValueResolverInterface
{
    /**
     * @return string
     */
    public function getId(): string
    {
        //ValueResolver can be referenced by YOUR_ID instead of the FQCN
        return 'YOUR_ID';
    }

    public function getDescription(): string
    {
        //will be shown when `spryker-sdk <task> -h` is called for each parameter
        return 'description';
    }

    public function getSettingPaths(): array
    {
        //ensures some_setting_path is read from settings and the respective value is passed
        //into getValue(['some_setting_path' => <value>])
        return [
            'some_setting_path',
        ];
    }

    public function getType(): string
    {
        //any php type
        return 'string';
    }

    public function getAlias(): ?string
    {
        //used to give an alias for overwriting the value via CLI `spryker-sdk <task> --some-alias=<value>`
        return 'some-alias';
    }

    /**
     * @param array<string, mixed> $settingValues
     * @return mixed
     */
    public function getValue(array $settingValues): mixed
    {
        //implementation to resolve the corresponding value
        //when null is returned the user of the SDK is asked to give his input
        return '<resolved value>'
    }

    /**
     * @return mixed
     */
    public function getDefaultValue(): mixed
    {
        //Used to set the default value for the CLI option
        return null;
    }
}

You can define a value resolver as a Symfony service, for example, to be able to inject services into it. If the value resolver is not defined as a service, it is instantiated by its FQCN.

Example of defining a value resolver as a Symfony service:

services:
  your_value_resolver:
    class: <YourNamespace>\ValueResolvers\YourValueResolver
    tags: ['sdk.value_resolver']

3. Add a setting

A bundle can add more settings that value resolvers can use to create a persistent behavior. You can define settings in the settings.yaml file and add them to the SDK by calling spryker-sdk setting:set setting_dirs <path to your settings>:

settings:
  - path: string #e.g.: some_setting
    initialization_description: string #Will be used when a user is asked to provide the setting value
    strategy: string #merge or overwrite, where merge will add the value to the list and overwrite will replace it
    init: bool #if the user should be asked for the setting value when `spryker-sdk sdk:init:sdk` or `spryker-sdk sdk:init:project` is called
    type: string #Use array for lists of values or any scalar type (string|integer|float|boolean)
    is_project: boolean #defines if the setting is persisted across projects and initialized during `spryker-sdk sdk:init:sdk` or per project and initialized with `spryker-sdk sdk:init:project`
    values: array|string|integer|float|boolean #serve as default values for initialization

4. Add a new command runner

Commands are executed via command runners. Each command has a type that determines what command runner can execute the command. To implement new task types, there must be a new command runner and you need to register it as a Symfony service.

Add a new command runner as shown in the example:

namespace <YourNamespace>\CommandRunners;

use SprykerSdk\Sdk\Contracts\CommandRunner\CommandRunnerInterface;

class YourTypeCommandRunner implements CommandRunnerInterface
{
    /**
     * @param CommandInterface $command
     *
     * @return bool
     */
    public function canHandle(CommandInterface $command): bool
    {
        return $command->getType() === 'your_command_type';
    }

    /**
     * @param CommandInterface $command
     * @param array<string, mixed> $resolvedValues
     *
     * @return int
     */
    public function execute(CommandInterface $command, array $resolvedValues): int
    {
        //own implementation on how to execute a command

        //MUST return 0 for success and 1-255 for failure
        return 0;
    }
}
  your_type_command_runner:
    class: <YourNamespace>\CommandRunners\YourTypeCommandRunner
    tags: ['command.runner']

Optionally, you can overwrite the existing command runners with a more suitable implementation:

  local_cli_command_runner:
    class: <YourNamespace>\CommandRunners\BetterLocalCliRunner