├── .github └── workflows │ └── test.yml ├── .gitignore ├── Annotation └── Supervisor.php ├── Command └── DumpCommand.php ├── DependencyInjection ├── Configuration.php └── MyBuilderSupervisorExtension.php ├── Exporter └── AnnotationSupervisorExporter.php ├── LICENSE ├── MyBuilderSupervisorBundle.php ├── README.md ├── Resources └── config │ └── services.yml ├── Tests ├── Command │ └── DumpCommandTest.php ├── DependencyInjection │ ├── MyBuilderSupervisorExtensionTest.php │ └── config │ │ ├── empty.yml │ │ └── full.yml ├── Fixtures │ ├── Command │ │ └── TestCommand.php │ └── app │ │ ├── AppKernel.php │ │ └── config │ │ └── test.yml ├── SupervisorTestCase.php └── bootstrap.php ├── composer.json └── phpunit.xml.dist /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Test 2 | 3 | on: [push] 4 | 5 | jobs: 6 | run: 7 | runs-on: ${{ matrix.operating-system }} 8 | strategy: 9 | matrix: 10 | operating-system: [ubuntu-latest] 11 | php-versions: ['7.4', '8.1'] 12 | symfony-versions: ['4.4.*', '5.4.*'] 13 | name: PHP ${{ matrix.php-versions }} Test on ${{ matrix.operating-system }} with Symfony ${{ matrix.symfony-versions }} 14 | steps: 15 | # —— Setup Github actions 🐙 ————————————————————————————————————————————— 16 | - name: Checkout 17 | uses: actions/checkout@v2 18 | 19 | # https://github.com/shivammathur/setup-php (community) 20 | - name: Setup PHP, with composer and extensions 21 | uses: shivammathur/setup-php@v2 22 | with: 23 | php-version: ${{ matrix.php-versions }} 24 | extensions: mbstring, xml, ctype, iconv, intl 25 | coverage: xdebug #optional 26 | 27 | # https://github.com/marketplace/actions/setup-php-action#problem-matchers 28 | - name: Setup problem matchers for PHP 29 | run: echo "::add-matcher::${{ runner.tool_cache }}/php.json" 30 | 31 | # https://github.com/marketplace/actions/setup-php-action#problem-matchers 32 | - name: Setup problem matchers for PHPUnit 33 | run: echo "::add-matcher::${{ runner.tool_cache }}/phpunit.json" 34 | 35 | # —— Composer 🧙‍️ ————————————————————————————————————————————————————————— 36 | - name: Get composer cache directory 37 | id: composer-cache 38 | run: echo "::set-output name=dir::$(composer config cache-files-dir)" 39 | 40 | - name: Cache composer dependencies 41 | uses: actions/cache@v1 42 | with: 43 | path: ${{ steps.composer-cache.outputs.dir }} 44 | # Use composer.json for key, if composer.lock is not committed. 45 | # key: ${{ runner.os }}-composer-${{ hashFiles('**/composer.json') }} 46 | key: ${{ runner.os }}-composer-${{ matrix.php-versions }}-${{ matrix.symfony-versions }}-${{ hashFiles('**/composer.json') }} 47 | restore-keys: ${{ runner.os }}-composer-${{ matrix.php-versions }}-${{ matrix.symfony-versions }}- 48 | 49 | - name: Validate Composer.json 50 | run: composer validate 51 | 52 | - name: Fix symfony version for symfony/framework-bundle 53 | run: composer require --no-update symfony/framework-bundle:"${{ matrix.symfony-versions }}"; 54 | 55 | - name: Fix symfony version for symfony/console 56 | run: composer require --no-update symfony/console:"${{ matrix.symfony-versions }}"; 57 | 58 | - name: Install Composer dependencies 59 | run: composer update --no-progress --no-suggest --prefer-dist --optimize-autoloader 60 | 61 | ## —— Test ✅ ——————————————————————————————————————————————————————————— 62 | - name: Run Tests 63 | run: php bin/phpunit --coverage-text 64 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | composer.lock 2 | composer.phar 3 | bin/ 4 | vendor/ 5 | var/ 6 | /Tests/Fixtures/app/cache/ 7 | .phpunit.result.cache 8 | -------------------------------------------------------------------------------- /Annotation/Supervisor.php: -------------------------------------------------------------------------------- 1 | annotationSupervisorExporter = $annotationSupervisorExporter; 21 | 22 | parent::__construct(); 23 | } 24 | 25 | protected function configure() 26 | { 27 | $this 28 | ->setName('supervisor:dump') 29 | ->setDescription('Dump the supervisor configuration for annotated commands') 30 | ->addOption('user', null, InputOption::VALUE_OPTIONAL, 'The desired user to invoke the command as') 31 | ->addOption('server', null, InputOption::VALUE_OPTIONAL, 'Only include programs for the specified server'); 32 | } 33 | 34 | protected function execute(InputInterface $input, OutputInterface $output): int 35 | { 36 | $commands = $this->getApplication()->all(); 37 | 38 | $output->write($this->annotationSupervisorExporter->export($commands, $this->parseOptions($input))); 39 | 40 | return 0; 41 | } 42 | 43 | private function parseOptions(InputInterface $input): array 44 | { 45 | $options = []; 46 | 47 | if ($user = $input->getOption('user')) { 48 | $options['user'] = $user; 49 | } 50 | 51 | if ($env = $input->getOption('env')) { 52 | $options['environment'] = $env; 53 | } 54 | 55 | if ($server = $input->getOption('server')) { 56 | $options['server'] = $server; 57 | } 58 | 59 | return $options; 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /DependencyInjection/Configuration.php: -------------------------------------------------------------------------------- 1 | getRootNode(); 14 | 15 | $rootNode 16 | ->children() 17 | ->arrayNode('exporter') 18 | ->children() 19 | ->variableNode('program')->end() 20 | ->scalarNode('executor')->example('php')->end() 21 | ->scalarNode('console')->example('bin/console')->end() 22 | ->end() 23 | ->end(); 24 | 25 | return $treeBuilder; 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /DependencyInjection/MyBuilderSupervisorExtension.php: -------------------------------------------------------------------------------- 1 | processConfiguration(new Configuration(), $configs); 15 | 16 | $loader = new Loader\YamlFileLoader($container, new FileLocator(__DIR__ . '/../Resources/config')); 17 | $loader->load('services.yml'); 18 | 19 | $exporterConfig = $config['exporter'] ?? []; 20 | $container->setParameter('mybuilder.supervisor_bundle.exporter_config', $exporterConfig); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /Exporter/AnnotationSupervisorExporter.php: -------------------------------------------------------------------------------- 1 | reader = $reader; 19 | $this->config = $config; 20 | } 21 | 22 | /** 23 | * @param Command[] $commands 24 | * @param array $options Runtime options that have been defined 25 | * 26 | * @return string The supervisor configuration 27 | */ 28 | public function export(array $commands, array $options): string 29 | { 30 | $programs = $this->toParsedPrograms($commands, $options); 31 | 32 | if (empty($programs)) { 33 | return ''; 34 | } 35 | 36 | $configuration = $this->buildConfiguration($programs); 37 | 38 | return $configuration->generate(); 39 | } 40 | 41 | private function toParsedPrograms(array $commands, array $options) 42 | { 43 | $config = isset($this->config['program']) ? (array) $this->config['program'] : []; 44 | [$env, $server, $options] = $this->takeEnvironmentAndServerFrom($options); 45 | $config += $options; 46 | 47 | return array_reduce( 48 | $commands, 49 | function (array $programs, Command $command) use ($env, $server, $config) { 50 | foreach ($this->toSupervisorAnnotations($command, $server) as $instance => $annotation) { 51 | $programs[] = $config + [ 52 | 'name' => $this->buildProgramName($command->getName(), $instance), 53 | 'command' => $this->buildCommand($command->getName(), $env, $annotation), 54 | 'numprocs' => $annotation->processes, 55 | ]; 56 | } 57 | 58 | return $programs; 59 | }, [] 60 | ); 61 | } 62 | 63 | private function takeEnvironmentAndServerFrom(array $options): array 64 | { 65 | $env = $options['environment'] ?? null; 66 | $server = $options['server'] ?? null; 67 | 68 | unset($options['environment'], $options['server']); 69 | 70 | return [$env, $server, $options]; 71 | } 72 | 73 | private function toSupervisorAnnotations(Command $command, $server): array 74 | { 75 | $annotations = $this->reader->getClassAnnotations(new \ReflectionClass($command)); 76 | 77 | $filtered = array_filter( 78 | $annotations, 79 | static function ($annotation) use ($server) { 80 | if ($annotation instanceof SupervisorAnnotation) { 81 | return null === $server || $server === $annotation->server; 82 | } 83 | 84 | return false; 85 | } 86 | ); 87 | 88 | if (empty($filtered)) { 89 | return []; 90 | } 91 | 92 | return array_combine(range(1, count($filtered)), array_values($filtered)); 93 | } 94 | 95 | private function buildProgramName(string $commandName, int $instance): string 96 | { 97 | $name = str_replace(':', '_', $commandName); 98 | 99 | return (1 === $instance) ? $name : "{$name}_{$instance}"; 100 | } 101 | 102 | private function buildCommand(string $commandName, string $environment, SupervisorAnnotation $annotation): string 103 | { 104 | $executor = ''; 105 | 106 | if ($annotation->executor) { 107 | $executor = $annotation->executor; 108 | } elseif (isset($this->config['executor'])) { 109 | $executor = $this->config['executor']; 110 | } 111 | 112 | $console = isset($this->config['console']) ? " {$this->config['console']}" : ''; 113 | $params = $annotation->params ? " {$annotation->params}" : ''; 114 | $env = $environment ? " --env=$environment" : ''; 115 | 116 | return $executor . $console . ' ' . $commandName . $params . $env; 117 | } 118 | 119 | private function buildConfiguration(array $programs): Configuration 120 | { 121 | $config = new Configuration; 122 | $config->registerProcessor(new CommandConfigurationProcessor); 123 | 124 | return array_reduce( 125 | $programs, 126 | static function (Configuration $config, $program) { 127 | $command = new \Francodacosta\Supervisord\Command; 128 | 129 | foreach ($program as $k => $v) { 130 | $command->set($k, $v); 131 | } 132 | 133 | $config->add($command); 134 | 135 | return $config; 136 | }, $config 137 | ); 138 | } 139 | } 140 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2013-2017 MyBuilder Limited 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in 11 | all copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | THE SOFTWARE. 20 | -------------------------------------------------------------------------------- /MyBuilderSupervisorBundle.php: -------------------------------------------------------------------------------- 1 | ['all' => true], 24 | ]; 25 | ``` 26 | 27 | ### Configure the bundle 28 | 29 | You can add the following to `packages/my_builder_supervisor.yaml` for Symfony to define your global export configuration: 30 | 31 | ```yaml 32 | my_builder_supervisor: 33 | exporter: 34 | # any Supervisor program options can be specified within this block 35 | program: 36 | autostart: 'true' 37 | 38 | # allows you to specify a program that all commands should be passed to 39 | executor: php 40 | 41 | # allows you to specify the console that all commands should be passed to 42 | console: bin/console 43 | ``` 44 | 45 | ## Usage 46 | 47 | The first step is to add the `use` case for the annotation to the top of the command you want to use the `@Supervisor` annotations in. 48 | 49 | ```php 50 | use MyBuilder\Bundle\SupervisorBundle\Annotation\Supervisor; 51 | ``` 52 | 53 | Then define the `@Supervisor` annotation within the command's PHPDoc, which tells Supervisor how to configure this program. 54 | The example below declares that three instances of this command should be running at all times on the server entitled 'web', with the provided parameter `--send`. 55 | 56 | ```php 57 | /** 58 | * Command for sending our email messages from the database. 59 | * 60 | * @Supervisor(processes=3, params="--send", server="web") 61 | */ 62 | class SendQueuedEmailsCommand extends Command {} 63 | ``` 64 | 65 | 66 | ## Exporting the Supervisor configuration 67 | 68 | You should run `bin/console supervisor:dump` and review what the Supervisor configuration will look like based on the current specified definition. 69 | If you are happy with this you can write out the configuration to a `conf` file: 70 | 71 | ``` 72 | $ bin/console supervisor:dump --user=mybuilder --server=web > "/etc/supervisor.d/symfony.conf" 73 | ``` 74 | 75 | And then reload Supervisor: 76 | 77 | ``` 78 | $ kill -SIGHUP $(supervisorctl pid) 79 | ``` 80 | 81 | ### Environment 82 | 83 | You can choose which environment you want to run the commands in Supervisor under like this: 84 | 85 | ``` 86 | $ bin/console supervisor:dump --server=web --env=prod 87 | ``` 88 | 89 | --- 90 | 91 | Created by [MyBuilder](http://www.mybuilder.com/) - Check out our [blog](http://tech.mybuilder.com/) for more insight into this and other open-source projects we release. 92 | -------------------------------------------------------------------------------- /Resources/config/services.yml: -------------------------------------------------------------------------------- 1 | services: 2 | mybuilder.supervisor_bundle.annotation_supervisor_exporter: 3 | class: MyBuilder\Bundle\SupervisorBundle\Exporter\AnnotationSupervisorExporter 4 | public: true 5 | arguments: 6 | - "@annotation_reader" 7 | - "%mybuilder.supervisor_bundle.exporter_config%" 8 | 9 | # Ensure command is loaded within Symfony 4 10 | MyBuilder\Bundle\SupervisorBundle\Command\DumpCommand: 11 | arguments: 12 | - '@mybuilder.supervisor_bundle.annotation_supervisor_exporter' 13 | tags: 14 | - { name: 'console.command' } 15 | -------------------------------------------------------------------------------- /Tests/Command/DumpCommandTest.php: -------------------------------------------------------------------------------- 1 | boot(); 19 | 20 | $application = new Application($kernel); 21 | $application->add(new TestCommand()); 22 | 23 | $this->command = $application->find('supervisor:dump'); 24 | } 25 | 26 | protected function tearDown(): void 27 | { 28 | self::ensureKernelShutdown(); 29 | } 30 | 31 | public function test_dump_export(): void 32 | { 33 | $commandTester = new CommandTester($this->command); 34 | $commandTester->execute( 35 | [ 36 | 'command' => $this->command->getName(), 37 | '--user' => 'mybuilder', 38 | '--server' => 'live', 39 | ] 40 | ); 41 | 42 | $expected = <<assertEquals($expected, $commandTester->getDisplay()); 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /Tests/DependencyInjection/MyBuilderSupervisorExtensionTest.php: -------------------------------------------------------------------------------- 1 | container = new ContainerBuilder(); 22 | $this->loader = new MyBuilderSupervisorExtension(); 23 | } 24 | 25 | /** 26 | * @dataProvider providerTestConfig 27 | */ 28 | public function test_config(array $expected, string $file): void 29 | { 30 | $this->loader->load($this->getConfig($file), $this->container); 31 | 32 | $this->assertEquals($expected, $this->container->getParameter('mybuilder.supervisor_bundle.exporter_config')); 33 | } 34 | 35 | public function providerTestConfig(): array 36 | { 37 | return [ 38 | [ 39 | [], 40 | 'empty.yml', 41 | ], 42 | [ 43 | [ 44 | 'program' => [ 45 | 'autostart' => 'true', 46 | ], 47 | 'executor' => 'php', 48 | 'console' => 'bin/console', 49 | ], 50 | 'full.yml', 51 | ], 52 | ]; 53 | } 54 | 55 | /** 56 | * Load the specified yaml config file. 57 | */ 58 | private function getConfig(string $fileName): array 59 | { 60 | $locator = new FileLocator(__DIR__ . '/config'); 61 | $file = $locator->locate($fileName, null, true); 62 | 63 | $config = Yaml::parse(file_get_contents($file)); 64 | 65 | if (null === $config) { 66 | return []; 67 | } 68 | 69 | return $config; 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /Tests/DependencyInjection/config/empty.yml: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mybuilder/supervisor-bundle/dc34a9424a630db63d53d588f056abe6d22e8139/Tests/DependencyInjection/config/empty.yml -------------------------------------------------------------------------------- /Tests/DependencyInjection/config/full.yml: -------------------------------------------------------------------------------- 1 | my_builder_supervisor: 2 | exporter: 3 | program: 4 | autostart: 'true' 5 | executor: php 6 | console: bin/console 7 | -------------------------------------------------------------------------------- /Tests/Fixtures/Command/TestCommand.php: -------------------------------------------------------------------------------- 1 | setName('supervisor:test-command'); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /Tests/Fixtures/app/AppKernel.php: -------------------------------------------------------------------------------- 1 | load(__DIR__ . '/config/' . $this->getEnvironment() . '.yml'); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /Tests/Fixtures/app/config/test.yml: -------------------------------------------------------------------------------- 1 | framework: 2 | secret: SECRET 3 | 4 | my_builder_supervisor: 5 | exporter: 6 | program: 7 | autostart: 'true' 8 | executor: php 9 | console: bin/console 10 | -------------------------------------------------------------------------------- /Tests/SupervisorTestCase.php: -------------------------------------------------------------------------------- 1 | =7.4", 19 | "doctrine/annotations": "~1.0", 20 | "francodacosta/supervisord": "~1.0", 21 | "symfony/config": "~4.0||~5.0", 22 | "symfony/console": "~4.0||~5.0", 23 | "symfony/framework-bundle": "~4.0||~5.0", 24 | "symfony/yaml": "^4.0||~5.0" 25 | }, 26 | "require-dev": { 27 | "phpunit/phpunit": "~9.0" 28 | }, 29 | "autoload": { 30 | "psr-4": { 31 | "MyBuilder\\Bundle\\SupervisorBundle\\": "" 32 | } 33 | }, 34 | "config": { 35 | "bin-dir": "bin", 36 | "sort-packages": true 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /phpunit.xml.dist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 16 | 17 | 18 | 19 | Tests 20 | 21 | 22 | 23 | 24 | 25 | . 26 | 27 | 28 | ./Tests 29 | ./vendor 30 | ./bin 31 | 32 | 33 | 34 | --------------------------------------------------------------------------------