├── .github
├── ISSUE_TEMPLATE
│ └── issue-template.md
└── workflows
│ ├── code-style.yml
│ └── run-tests.yml
├── .gitignore
├── CHANGELOG.md
├── LICENCE
├── README.md
├── UPGRADING.md
├── app.Dockerfile
├── composer.json
├── composer.lock
├── config
└── cloud-scheduler.php
├── docker-compose.yml
├── example.png
├── logo.png
├── phpunit.xml
├── schedule-command-example.png
├── src
├── CloudSchedulerException.php
├── CloudSchedulerServiceProvider.php
├── Command.php
├── OpenIdVerificator.php
├── OpenIdVerificatorConcrete.php
├── OpenIdVerificatorFake.php
└── TaskHandler.php
├── testbench.yaml
├── tests
├── TaskHandlerTest.php
└── TestCase.php
├── use-package-kernel.php
└── workbench
├── .env
├── .gitignore
├── app
├── Console
│ ├── Commands
│ │ ├── TestCommand.php
│ │ └── TestCommand2.php
│ └── Kernel.php
└── Models
│ └── .gitkeep
├── database
├── factories
│ └── .gitkeep
├── migrations
│ └── .gitkeep
└── seeders
│ ├── .gitkeep
│ └── DatabaseSeeder.php
├── resources
└── views
│ └── .gitkeep
└── routes
├── .gitkeep
├── api.php
├── console.php
└── web.php
/.github/ISSUE_TEMPLATE/issue-template.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Issue template
3 | about: Help you create an effective issue and know what to expect.
4 | title: ''
5 | labels: ''
6 | assignees: ''
7 |
8 | ---
9 |
10 | Thank you for taking the time for reporting a bug, requesting a feature, or letting me know something else about the package.
11 |
12 | I create open-source packages for fun while working full-time and running my own business. That means I don't have as much time left to maintain these packages, build elaborate new features or investigate and fix bugs. If you wish to get a feature or bugfix merged it would be greatly appreciated if you can provide as much info as possible and preferably a Pull Request ready with automated tests. Realistically I check Github a few times a week, and take several days, weeks or sometimes months before finishing features/bugfixes (depending on their size of course).
13 |
14 | Thanks for understanding. 😁
15 |
--------------------------------------------------------------------------------
/.github/workflows/code-style.yml:
--------------------------------------------------------------------------------
1 | name: Code style
2 |
3 | on:
4 | push:
5 | paths:
6 | - '**.php'
7 |
8 | permissions:
9 | contents: write
10 |
11 | jobs:
12 | php-code-styling:
13 | runs-on: ubuntu-latest
14 | timeout-minutes: 5
15 |
16 | steps:
17 | - name: Checkout code
18 | uses: actions/checkout@v4
19 | with:
20 | ref: ${{ github.head_ref }}
21 |
22 | - name: Check code style
23 | uses: aglipanci/laravel-pint-action@v2
24 |
25 | - name: Commit changes
26 | uses: stefanzweifel/git-auto-commit-action@v5
27 | with:
28 | commit_message: Apply code style rules
--------------------------------------------------------------------------------
/.github/workflows/run-tests.yml:
--------------------------------------------------------------------------------
1 | name: Run tests
2 |
3 | on:
4 | pull_request_target:
5 | types: [opened, synchronize, labeled]
6 | schedule:
7 | - cron: '0 0 * * *'
8 |
9 | jobs:
10 | access_check:
11 | runs-on: ubuntu-latest
12 | name: Access check
13 | steps:
14 | - name: Ensure pull-request is safe to run
15 | uses: actions/github-script@v7
16 | with:
17 | github-token: ${{secrets.GITHUB_TOKEN}}
18 | script: |
19 | if (context.eventName === 'schedule') {
20 | return
21 | }
22 |
23 | // If the user that pushed the commit is a maintainer, skip the check
24 | const collaborators = await github.rest.repos.listCollaborators({
25 | owner: context.repo.owner,
26 | repo: context.repo.repo
27 | });
28 |
29 | if (collaborators.data.some(c => c.login === context.actor)) {
30 | console.log(`User ${context.actor} is allowed to run tests because they are a collaborator.`);
31 | return
32 | }
33 |
34 | const issue_number = context.issue.number;
35 | const repository = context.repo.repo;
36 | const owner = context.repo.owner;
37 |
38 | const response = await github.rest.issues.listLabelsOnIssue({
39 | owner,
40 | repo: repository,
41 | issue_number
42 | });
43 | const labels = response.data.map(label => label.name);
44 | let hasLabel = labels.includes('safe-to-test')
45 |
46 | if (context.payload.action === 'synchronize' && hasLabel) {
47 | hasLabel = false
48 | await github.rest.issues.removeLabel({
49 | owner,
50 | repo: repository,
51 | issue_number,
52 | name: 'safe-to-test'
53 | });
54 | }
55 |
56 | if (!hasLabel) {
57 | throw "Action was not authorized. Exiting now."
58 | }
59 |
60 | php-tests:
61 | runs-on: ubuntu-latest
62 | needs: access_check
63 | strategy:
64 | matrix:
65 | payload:
66 | - { laravel: '11.*', php: '8.4', 'testbench': '9.*', collision: '8.*'}
67 | - { laravel: '11.*', php: '8.3', 'testbench': '9.*', collision: '8.*'}
68 | - { laravel: '11.*', php: '8.2', 'testbench': '9.*', collision: '8.*'}
69 | - { laravel: '12.*', php: '8.4', 'testbench': '10.*', collision: '8.*'}
70 | - { laravel: '12.*', php: '8.3', 'testbench': '10.*', collision: '8.*'}
71 | - { laravel: '12.*', php: '8.2', 'testbench': '10.*', collision: '8.*'}
72 |
73 | name: PHP ${{ matrix.payload.php }} - Laravel ${{ matrix.payload.laravel }}
74 |
75 | steps:
76 | - name: Checkout code
77 | uses: actions/checkout@v4
78 | with:
79 | ref: ${{ github.event.pull_request.head.sha }}
80 |
81 | - name: Setup PHP
82 | uses: shivammathur/setup-php@v2
83 | with:
84 | php-version: ${{ matrix.payload.php }}
85 | extensions: mbstring, dom, fileinfo
86 | coverage: none
87 |
88 | - name: Install dependencies
89 | run: |
90 | composer require "laravel/framework:${{ matrix.payload.laravel }}" "orchestra/testbench:${{ matrix.payload.testbench }}" "nunomaduro/collision:${{ matrix.payload.collision }}" --no-interaction --no-update
91 | composer update --prefer-stable --prefer-dist --no-interaction
92 | - name: Execute tests
93 | run: |
94 | php -v | head -n 1
95 | vendor/bin/testbench | head -n 1
96 | composer test
97 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | /vendor/
2 | .idea/
3 | .DS_Store
4 | .phpunit.result.cache
5 | .phpunit.cache/
6 |
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | # Releases
2 | All notable changes to this project will be documented in this file.
3 |
4 | The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/)
5 | and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html).
6 |
7 | # 2.0.5 - 2023-12-28
8 |
9 | **Changed**
10 |
11 | - Test package with PHP 8.3
12 |
13 | # 2.0.4 - 2023-10-02
14 |
15 | **Changed**
16 |
17 | - Update phpseclib to 3.x
18 |
19 | # 2.0.3 - 2023-06-06
20 |
21 | **Changed**
22 |
23 | - Downgrade phpseclib back to 2.x
24 |
25 | # 2.0.2 - 2023-06-03
26 |
27 | **Changed**
28 |
29 | - Bump phpseclib to 3.x
30 |
31 | # 2.0.1 - 2022-04-26
32 |
33 | **Fixed**
34 |
35 | - Fix conflicting facade accessor (in #16)
36 |
37 | # 2.0.0 - 2022-04-23
38 |
39 | **Changed**
40 |
41 | - Dropped older PHP and Laravel support
42 | - Bumped dependencies
43 |
44 | ## 1.1.0 - 2022-02-09
45 |
46 | **Changed**
47 |
48 | - Added Laravel 9 support.
49 |
50 | ## 1.0.4 - 2022-02-01
51 |
52 | **Added**
53 |
54 | - Added retry mechanism for Google APIs to try 3 times in case they fail
55 |
56 | ## 1.0.3 - 2022-01-08
57 |
58 | **Changed**
59 |
60 | - Updated Composer dependencies
61 |
62 | ## 1.0.2 - 2020-12-27
63 |
64 | **Fixed**
65 |
66 | - Undefined offset error for closure commands
67 |
68 | ## 1.0.1 - 2020-12-07
69 |
70 | **Fixed**
71 |
72 | - Fixed certificates cached too long
73 |
74 | ## 1.0.0 - 2020-10-11
75 |
76 | **Added**
77 |
78 | - Release of the package
79 |
80 | ## 1.0.0-alpha1 - 2020-06-26
81 |
82 | **Added**
83 |
84 | - Initial release of the package.
85 |
--------------------------------------------------------------------------------
/LICENCE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2020 Stackkit (info@stackkit.io)
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 |
3 | [](https://github.com/stackkit/laravel-google-cloud-scheduler/actions/workflows/run-tests.yml)
4 |
5 |
6 |
7 | Companion packages: Cloud Tasks, Cloud Logging
8 |
9 | # Introduction
10 |
11 | This package allows you to use Google Cloud Scheduler to schedule Laravel commands.
12 |
13 | # How it works
14 |
15 | Cloud Scheduler will make HTTP calls to your application. This package adds an endpoint to your application that accepts these HTTP calls with their payload (an Artisan command) and execute them.
16 |
17 | There are two ways to schedule commands using this package:
18 |
19 |
20 | 1. Schedule the `schedule:run` command
21 |
22 | This is the easiest way to use this package. You can schedule the `schedule:run` command to run every minute.
23 |
24 |
25 |
26 | 2. Schedule commands separately
27 |
28 | If your application does not have commands that should run every minute, you may choose to schedule them individually.
29 |
30 | If the command uses `withoutOverlapping`, `before`, `after`, `onSuccess`, `thenPing`, etc, this package will respect those settings, as long as the command is also scheduled in the console kernel.
31 |
32 | For example, let's say we have to generate a report every day at 3:00 AM. We can schedule the `reports:generate` command to run at 3:00 AM using Cloud Scheduler. After the report is generated, OhDear should be pinged.
33 |
34 | Firstly, schedule the command in Cloud Tasks:
35 |
36 |
37 |
38 | Then, schedule the command in the console kernel:
39 |
40 | ```php
41 | public function schedule(Schedule $schedule)
42 | {
43 | $schedule->command('report:generate')
44 | ->thenPing('https://ohdear.app/ping');
45 | }
46 | ```
47 |
48 | The package will pick up on the scheduled settings and ping OhDear after the command has run.
49 |
50 |
51 | # Requirements
52 |
53 | This package requires Laravel 11 or 12.
54 |
55 | # Installation
56 |
57 | 1 - Require the package using Composer
58 |
59 | ```bash
60 | composer require stackkit/laravel-google-cloud-scheduler
61 | ```
62 |
63 | 2 - Define environment variables
64 |
65 | `CLOUD_SCHEDULER_APP_URL` - This should be the URL defined in the `URL` field of your Cloud Scheduler job.
66 |
67 | `CLOUD_SCHEDULER_SERVICE_ACCOUNT` - The e-mail address of the service account invocating the task.
68 |
69 | Optionally, you may publish the configuration file:
70 |
71 | ```bash
72 | php artisan vendor:publish --tag=cloud-scheduler-config
73 | ```
74 |
75 | 3 - Ensure PHP executable is in open_basedir. This is required for the package to run Artisan commands.
76 |
77 | How to find the executable:
78 |
79 | ```php
80 | php artisan tinker --execute="(new Symfony\\Component\\Process\\PhpExecutableFinder())->find()"
81 | ```
82 |
83 | 4 - Optional, but highly recommended: server configuration
84 |
85 | Since Artisan commands are now invoked via an HTTP request, you might encounter issues with timeouts. Here's how to adjust them:
86 |
87 | ```nginx
88 | server {
89 | # other server configuration ...
90 |
91 | location /cloud-scheduler-job {
92 | proxy_connect_timeout 600s;
93 | proxy_read_timeout 600s;
94 | fastcgi_read_timeout 600s;
95 | }
96 |
97 | # other locations and server configuration ...
98 | }
99 |
100 | ```
101 |
102 | 5 - Optional, but highly recommended: set application `RUNNING_IN_CONSOLE`
103 |
104 | Some Laravel service providers only register their commands if the application is being accessed through the command line (Artisan). Because we are calling Laravel scheduler from a HTTP call, that means some commands may never register, such as the Laravel Scout command:
105 |
106 | ```php
107 | /**
108 | * Bootstrap any application services.
109 | *
110 | * @return void
111 | */
112 | public function boot()
113 | {
114 | if ($this->app->runningInConsole()) {
115 | $this->commands([
116 | FlushCommand::class,
117 | ImportCommand::class,
118 | IndexCommand::class,
119 | DeleteIndexCommand::class,
120 | ]);
121 |
122 | $this->publishes([
123 | __DIR__.'/../config/scout.php' => $this->app['path.config'].DIRECTORY_SEPARATOR.'scout.php',
124 | ]);
125 | }
126 | }
127 | ```
128 |
129 | To circumvent this, please add the following to `bootstrap/app.php`
130 |
131 | ```php
132 | withRouting(
144 | web: __DIR__.'/../routes/web.php',
145 | commands: __DIR__.'/../routes/console.php',
146 | health: '/up',
147 | )
148 | ->withMiddleware(function (Middleware $middleware) {
149 | //
150 | })
151 | ->withExceptions(function (Exceptions $exceptions) {
152 | //
153 | })->create();
154 |
155 | ```
156 |
157 | 6 - Optional: whitelist route for maintenance mode
158 |
159 | If you want to allow jobs to keep running if the application is down (`php artisan down`), update the following:
160 |
161 | ```php
162 | return Application::configure(basePath: dirname(__DIR__))
163 | ->withRouting(
164 | web: __DIR__ . '/../routes/web.php',
165 | commands: __DIR__ . '/../routes/console.php',
166 | health: '/up',
167 | )
168 | ->withMiddleware(function (Middleware $middleware) {
169 | $middleware->preventRequestsDuringMaintenance(
170 | except: [
171 | '/cloud-scheduler-job',
172 | ],
173 | );
174 | })
175 | ->withExceptions(function (Exceptions $exceptions) {
176 | //
177 | })->create();
178 |
179 |
180 | ```
181 |
182 | # Cloud Scheduler Example
183 |
184 | Here is an example job that will run `php artisan schedule:run` every minute.
185 |
186 | These are the most important settings:
187 | - Target must be `HTTP`
188 | - URL and AUD (audience) must be `https://yourdomainname.com/cloud-scheduler-job`
189 | - Auth header must be OIDC token!
190 |
191 |
192 |
193 | # Security
194 |
195 | The job handler requires each request to have an OpenID token. Cloud Scheduler will generate an OpenID token and send it along with the job payload to the handler.
196 |
197 | This package verifies that the token is digitally signed by Google and that it's meant for your application. Only Google Scheduler will be able to call your handler.
198 |
199 | More information about OpenID Connect:
200 |
201 | https://developers.google.com/identity/protocols/oauth2/openid-connect
202 |
--------------------------------------------------------------------------------
/UPGRADING.md:
--------------------------------------------------------------------------------
1 | # From 2.x to 3.x
2 |
3 | Support for Laravel 6, 7, 8, and 9 has been dropped. The minimum supported version is now Laravel 10.
4 |
5 | ## Environment changes (Impact: high)
6 |
7 | Publish the new configuration file:
8 |
9 | ```bash
10 | php artisan vendor:publish --tag=cloud-scheduler-config
11 | ```
12 |
13 | Change the environment variables names:
14 |
15 | - `STACKKIT_CLOUD_SCHEDULER_APP_URL` -> `CLOUD_SCHEDULER_APP_URL`
16 |
17 | Add the following environment variable:
18 |
19 | - `CLOUD_SCHEDULER_SERVICE_ACCOUNT` - The e-mail address of the service account invocating the task.
20 |
--------------------------------------------------------------------------------
/app.Dockerfile:
--------------------------------------------------------------------------------
1 | FROM serversideup/php:8.3-fpm
2 |
3 | USER root
4 | RUN install-php-extensions bcmath
5 |
6 | USER www-data
--------------------------------------------------------------------------------
/composer.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "stackkit/laravel-google-cloud-scheduler",
3 | "license": "MIT",
4 | "authors": [
5 | {
6 | "name": "Marick van Tuil",
7 | "email": "info@marickvantuil.nl"
8 | }
9 | ],
10 | "require": {
11 | "ext-json": "*",
12 | "google/cloud-scheduler": "^2.0",
13 | "google/auth": "^v1.29.1",
14 | "laravel/framework": "^11.0|^12.0",
15 | "symfony/cache": "^7.2",
16 | "phpseclib/phpseclib": "^3.0"
17 | },
18 | "require-dev": {
19 | "orchestra/testbench": "^9.0|^10.0",
20 | "nunomaduro/collision": "^8.0",
21 | "laravel/pint": "^1.14"
22 | },
23 | "autoload": {
24 | "psr-4": {
25 | "Stackkit\\LaravelGoogleCloudScheduler\\": "src/"
26 | }
27 | },
28 | "autoload-dev": {
29 | "psr-4": {
30 | "Tests\\": "tests/",
31 | "Workbench\\App\\": "workbench/app/",
32 | "Workbench\\Database\\Factories\\": "workbench/database/factories/",
33 | "Workbench\\Database\\Seeders\\": "workbench/database/seeders/"
34 | }
35 | },
36 | "extra": {
37 | "laravel": {
38 | "providers": [
39 | "Stackkit\\LaravelGoogleCloudScheduler\\CloudSchedulerServiceProvider"
40 | ]
41 | }
42 | },
43 | "scripts": {
44 | "test": "testbench package:test --ansi",
45 | "post-autoload-dump": [
46 | "@clear",
47 | "@prepare"
48 | ],
49 | "clear": "@php vendor/bin/testbench package:purge-skeleton --ansi",
50 | "prepare": [
51 | "@php vendor/bin/testbench package:discover --ansi",
52 | "@php use-package-kernel.php"
53 | ],
54 | "build": "@php vendor/bin/testbench workbench:build --ansi",
55 | "serve": [
56 | "Composer\\Config::disableProcessTimeout",
57 | "@build",
58 | "@php vendor/bin/testbench serve"
59 | ],
60 | "lint": [
61 | "@php vendor/bin/phpstan analyse"
62 | ],
63 | "test": "testbench package:test",
64 | "l10": [
65 | "composer require laravel/framework:10.* orchestra/testbench:8.* nunomaduro/collision:7.* --no-interaction --no-update",
66 | "composer update --prefer-stable --prefer-dist --no-interaction"
67 | ],
68 | "l11": [
69 | "composer require laravel/framework:11.* orchestra/testbench:9.* nunomaduro/collision:8.* --no-interaction --no-update",
70 | "composer update --prefer-stable --prefer-dist --no-interaction"
71 | ]
72 | },
73 | "minimum-stability": "dev",
74 | "prefer-stable": true
75 | }
76 |
--------------------------------------------------------------------------------
/config/cloud-scheduler.php:
--------------------------------------------------------------------------------
1 | env('CLOUD_SCHEDULER_APP_URL'),
5 | 'service_account' => env('CLOUD_SCHEDULER_SERVICE_ACCOUNT'),
6 | 'disable_task_handler' => false,
7 | 'disable_token_verification' => false,
8 | ];
9 |
--------------------------------------------------------------------------------
/docker-compose.yml:
--------------------------------------------------------------------------------
1 | services:
2 | app:
3 | build:
4 | context: .
5 | dockerfile: app.Dockerfile
6 | volumes:
7 | - .:/var/www/html
--------------------------------------------------------------------------------
/example.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/stackkit/laravel-google-cloud-scheduler/b2b046c3cd417bca207d2cc84a38146dd0b799af/example.png
--------------------------------------------------------------------------------
/logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/stackkit/laravel-google-cloud-scheduler/b2b046c3cd417bca207d2cc84a38146dd0b799af/logo.png
--------------------------------------------------------------------------------
/phpunit.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | ./tests/
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 | src/
19 |
20 |
21 |
22 |
--------------------------------------------------------------------------------
/schedule-command-example.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/stackkit/laravel-google-cloud-scheduler/b2b046c3cd417bca207d2cc84a38146dd0b799af/schedule-command-example.png
--------------------------------------------------------------------------------
/src/CloudSchedulerException.php:
--------------------------------------------------------------------------------
1 | registerRoutes($router);
13 | $this->registerClient();
14 | }
15 |
16 | public function register()
17 | {
18 | $this->mergeConfigFrom(__DIR__.'/../config/cloud-scheduler.php', 'cloud-scheduler');
19 |
20 | $this->publishes([
21 | __DIR__.'/../config/cloud-scheduler.php' => config_path('cloud-scheduler.php'),
22 | ], 'cloud-scheduler-config');
23 | }
24 |
25 | private function registerRoutes(Router $router)
26 | {
27 | $router->post('cloud-scheduler-job', [TaskHandler::class, 'handle']);
28 | }
29 |
30 | private function registerClient()
31 | {
32 | $this->app->bind('open-id-verificator-gcs', OpenIdVerificatorConcrete::class);
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/src/Command.php:
--------------------------------------------------------------------------------
1 | getContent();
10 | }
11 |
12 | public function captureWithoutArtisan()
13 | {
14 | return trim(str_replace('php artisan', '', $this->capture()));
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/src/OpenIdVerificator.php:
--------------------------------------------------------------------------------
1 | accessToken = $accessToken;
15 | }
16 |
17 | public function verify(?string $token, array $config): void
18 | {
19 | if (! $token) {
20 | throw new CloudSchedulerException('Missing [Authorization] header');
21 | }
22 |
23 | $payload = $this->accessToken->verify(
24 | $token,
25 | [
26 | 'audience' => config('cloud-scheduler.app_url'),
27 | 'throwException' => true,
28 | ]
29 | );
30 |
31 | if (($payload['email'] ?? '') !== config('cloud-scheduler.service_account')) {
32 | throw new CloudSchedulerException('Invalid service account email');
33 | }
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/src/OpenIdVerificatorFake.php:
--------------------------------------------------------------------------------
1 | schedule = $container->make(Schedule::class);
20 | }
21 |
22 | /**
23 | * @throws CloudSchedulerException
24 | */
25 | public function handle()
26 | {
27 | if (config('cloud-scheduler.disable_task_handler')) {
28 | abort(404);
29 | }
30 |
31 | if (config('cloud-scheduler.disable_token_verification') !== true) {
32 | OpenIdVerificator::verify(request()->bearerToken(), []);
33 | }
34 |
35 | set_time_limit(0);
36 |
37 | $output = $this->runCommand($this->command->captureWithoutArtisan());
38 |
39 | return $this->cleanOutput($output);
40 | }
41 |
42 | private function runCommand($command)
43 | {
44 | if ($this->isScheduledCommand($command)) {
45 | $scheduledCommand = $this->getScheduledCommand($command);
46 |
47 | if ($scheduledCommand->withoutOverlapping && ! $scheduledCommand->mutex->create($scheduledCommand)) {
48 | return null;
49 | }
50 |
51 | $scheduledCommand->callBeforeCallbacks($this->container);
52 |
53 | Artisan::call($command);
54 |
55 | $scheduledCommand->callAfterCallbacks($this->container);
56 | } else {
57 | Artisan::call($command);
58 | }
59 |
60 | return Artisan::output();
61 | }
62 |
63 | private function isScheduledCommand($command)
64 | {
65 | return ! is_null($this->getScheduledCommand($command));
66 | }
67 |
68 | private function getScheduledCommand($command)
69 | {
70 | $events = $this->schedule->events();
71 |
72 | foreach ($events as $event) {
73 | if (! is_string($event->command)) {
74 | continue;
75 | }
76 |
77 | $eventCommand = $this->commandWithoutArtisan($event->command);
78 |
79 | if ($command === $eventCommand) {
80 | return $event;
81 | }
82 | }
83 |
84 | return null;
85 | }
86 |
87 | private function commandWithoutArtisan($command)
88 | {
89 | $parts = explode(ARTISAN_BINARY, $command);
90 |
91 | return substr($parts[1], 2, strlen($parts[1]));
92 | }
93 |
94 | private function cleanOutput($output)
95 | {
96 | return trim($output);
97 | }
98 | }
99 |
--------------------------------------------------------------------------------
/testbench.yaml:
--------------------------------------------------------------------------------
1 | providers:
2 | - Stackkit\LaravelGoogleCloudScheduler\CloudSchedulerServiceProvider
3 |
4 | migrations:
5 | - workbench/database/migrations
6 |
7 | seeders:
8 | - Workbench\Database\Seeders\DatabaseSeeder
9 |
10 | workbench:
11 | start: '/'
12 | install: true
13 | discovers:
14 | web: true
15 | api: false
16 | commands: true
17 | components: false
18 | views: false
19 | build: []
20 | assets: []
21 | sync: []
22 |
--------------------------------------------------------------------------------
/tests/TaskHandlerTest.php:
--------------------------------------------------------------------------------
1 | call('POST', '/cloud-scheduler-job', content: 'php artisan env')->content();
20 |
21 | // Assert
22 | $this->assertStringContainsString('The application environment is [testing]', $output);
23 | }
24 |
25 | #[Test]
26 | public function it_requires_a_jwt()
27 | {
28 | // Act
29 | $response = $this->call('POST', '/cloud-scheduler-job', content: 'php artisan env');
30 |
31 | // Assert
32 | $this->assertStringContainsString('Missing [Authorization] header', $response->content());
33 | $response->assertStatus(500);
34 |
35 | }
36 |
37 | #[Test]
38 | public function it_requires_a_jwt_signed_by_google()
39 | {
40 | // Act
41 | $response = $this
42 | ->withToken('hey')
43 | ->call('POST', '/cloud-scheduler-job', server: ['HTTP_AUTHORIZATION' => 'Bearer 123'], content: 'php artisan env');
44 |
45 | // Assert
46 | $this->assertStringContainsString('Wrong number of segments', $response->content());
47 | $response->assertStatus(500);
48 | }
49 |
50 | #[Test]
51 | public function it_prevents_overlapping_if_the_command_is_scheduled_without_overlapping()
52 | {
53 | OpenIdVerificator::fake();
54 | Event::fake();
55 |
56 | cache()->clear();
57 |
58 | $this->assertLoggedLines(0);
59 |
60 | $this->call('POST', '/cloud-scheduler-job', content: 'php artisan test:command');
61 |
62 | $this->assertLoggedLines(1);
63 | $this->assertLogged('TestCommand');
64 |
65 | $mutex = head(app(Schedule::class)->events())->mutexName();
66 |
67 | cache()->add($mutex, true, 60);
68 |
69 | $this->call('POST', '/cloud-scheduler-job', content: 'php artisan test:command');
70 |
71 | $this->assertLoggedLines(1);
72 |
73 | cache()->delete($mutex);
74 |
75 | $this->call('POST', '/cloud-scheduler-job', content: 'php artisan test:command');
76 |
77 | $this->assertLoggedLines(2);
78 | }
79 |
80 | #[Test]
81 | public function it_runs_the_before_and_after_callbacks()
82 | {
83 | OpenIdVerificator::fake();
84 |
85 | $this->call('POST', '/cloud-scheduler-job', content: 'php artisan test:command2');
86 |
87 | $this->assertLoggedLines(3);
88 | $this->assertLogged('log after');
89 | $this->assertLogged('log before');
90 | $this->assertLogged('TestCommand2');
91 | }
92 |
93 | #[Test]
94 | public function it_can_run_the_schedule_run_command()
95 | {
96 | OpenIdVerificator::fake();
97 |
98 | $this->call('POST', '/cloud-scheduler-job', content: 'php artisan schedule:run');
99 |
100 | $this->assertLoggedLines(5);
101 | $this->assertLogged('TestCommand');
102 | $this->assertLogged('TestCommand2');
103 | $this->assertLogged('log call');
104 | $this->assertLogged('log after');
105 | $this->assertLogged('log before');
106 | }
107 | }
108 |
--------------------------------------------------------------------------------
/tests/TestCase.php:
--------------------------------------------------------------------------------
1 | resetLog();
23 |
24 | cache()->clear();
25 | }
26 |
27 | public function assertLogged(string $message): void
28 | {
29 | $log = file_get_contents(storage_path('log.txt'));
30 | $this->assertStringContainsString($message, $log);
31 | }
32 |
33 | public function assertLoggedLines(int $lines): void
34 | {
35 | $log = file_get_contents(storage_path('log.txt'));
36 | $this->assertCount($lines, array_filter(explode("\n", $log)));
37 | }
38 |
39 | public function resetLog(): void
40 | {
41 | file_put_contents(storage_path('log.txt'), '');
42 | }
43 | }
44 |
--------------------------------------------------------------------------------
/use-package-kernel.php:
--------------------------------------------------------------------------------
1 | command(TestCommand::class)->withoutOverlapping()->everyMinute();
19 | $schedule->command('test:command2')->before(function () {
20 | file_put_contents(storage_path('log.txt'), 'log after'.PHP_EOL, FILE_APPEND);
21 | })->after(function () {
22 | file_put_contents(storage_path('log.txt'), 'log before'.PHP_EOL, FILE_APPEND);
23 | });
24 | $schedule->call(function () {
25 | file_put_contents(storage_path('log.txt'), 'log call'.PHP_EOL, FILE_APPEND);
26 | });
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/workbench/app/Models/.gitkeep:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/stackkit/laravel-google-cloud-scheduler/b2b046c3cd417bca207d2cc84a38146dd0b799af/workbench/app/Models/.gitkeep
--------------------------------------------------------------------------------
/workbench/database/factories/.gitkeep:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/stackkit/laravel-google-cloud-scheduler/b2b046c3cd417bca207d2cc84a38146dd0b799af/workbench/database/factories/.gitkeep
--------------------------------------------------------------------------------
/workbench/database/migrations/.gitkeep:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/stackkit/laravel-google-cloud-scheduler/b2b046c3cd417bca207d2cc84a38146dd0b799af/workbench/database/migrations/.gitkeep
--------------------------------------------------------------------------------
/workbench/database/seeders/.gitkeep:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/stackkit/laravel-google-cloud-scheduler/b2b046c3cd417bca207d2cc84a38146dd0b799af/workbench/database/seeders/.gitkeep
--------------------------------------------------------------------------------
/workbench/database/seeders/DatabaseSeeder.php:
--------------------------------------------------------------------------------
1 | get('/user', function (Request $request) {
18 | // return $request->user();
19 | // });
20 |
--------------------------------------------------------------------------------
/workbench/routes/console.php:
--------------------------------------------------------------------------------
1 | comment(Inspiring::quote());
19 | // })->purpose('Display an inspiring quote');
20 |
--------------------------------------------------------------------------------
/workbench/routes/web.php:
--------------------------------------------------------------------------------
1 |