├── test
├── fixtures
│ ├── clonedir
│ │ ├── file1
│ │ ├── file2
│ │ └── file3
│ ├── appengine-standard
│ │ ├── app-php72.yaml.dist
│ │ ├── app-php55.yaml.dist
│ │ ├── composer.json
│ │ ├── app.php
│ │ ├── index.php
│ │ ├── phpunit.xml.dist
│ │ └── tests
│ │ │ ├── e2e
│ │ │ ├── HelloTestTrait.php
│ │ │ ├── LocalTest.php
│ │ │ └── DeployTest.php
│ │ │ └── unit
│ │ │ └── HelloTest.php
│ └── snippet2.php
├── src
│ ├── snippet3.php
│ ├── snippet1.php
│ ├── function_snippet_invalid.php
│ └── function_snippet1.php
├── bin
│ └── cloud_sql_proxy
├── Utils
│ ├── Flex
│ │ ├── data
│ │ │ ├── basic-cloudbuild-yaml
│ │ │ ├── cloudsql-cloudbuild-yaml
│ │ │ ├── basic-describe-result
│ │ │ └── cloudsql-describe-result
│ │ └── FlexExecCommandTest.php
│ ├── mocked_exec.php
│ ├── WordPress
│ │ ├── DeployTest.php
│ │ ├── wpGaeTest.php
│ │ └── ProjectTest.php
│ ├── ProjectTest.php
│ ├── ContainerExecTest.php
│ └── GcloudTest.php
├── TestUtils
│ ├── sleeper.php
│ ├── command.php
│ ├── ExecuteCommandTraitTest.php
│ ├── CloudSqlProxyTraitTest.php
│ ├── FileUtilTest.php
│ ├── ExponentialBackoffTraitTest.php
│ ├── GcloudWrapper
│ │ └── CloudRunTest.php
│ └── EventuallyConsistentTestTraitTest.php
├── bootstrap.php
└── Fixers
│ └── ClientUpgradeFixerTest.php
├── examples
├── .gitignore
├── renovate.json
├── src
├── Utils
│ ├── WordPress
│ │ ├── files
│ │ │ ├── cron.yaml
│ │ │ ├── php.ini
│ │ │ ├── app.yaml
│ │ │ ├── gae-app.php
│ │ │ └── wp-config.php
│ │ └── wp-gae
│ ├── templates
│ │ └── cloudbuild.yaml.tmpl
│ ├── Flex
│ │ └── flex_exec
│ ├── Gcloud.php
│ ├── ContainerExec.php
│ ├── ExponentialBackoff.php
│ └── Project.php
├── Fixers
│ └── ClientUpgradeFixer
│ │ ├── examples
│ │ ├── no_args.legacy.php
│ │ ├── no_args.new.php
│ │ ├── non_rpc_methods.legacy.php
│ │ ├── required_args.legacy.php
│ │ ├── non_rpc_methods.new.php
│ │ ├── mixins.legacy.php
│ │ ├── required_args.new.php
│ │ ├── multiple_clients.legacy.php
│ │ ├── vars_in_constructor.legacy.php
│ │ ├── optional_args.legacy.php
│ │ ├── optional_args_array_keyword.legacy.php
│ │ ├── optional_args_variable.legacy.php
│ │ ├── mixins.new.php
│ │ ├── multiple_clients.new.php
│ │ ├── required_and_optional_args.legacy.php
│ │ ├── class_vars.legacy.php
│ │ ├── vars_in_constructor.new.php
│ │ ├── optional_args.new.php
│ │ ├── optional_args_array_keyword.new.php
│ │ ├── inline_html.legacy.php
│ │ ├── optional_args_variable.new.php
│ │ ├── var_typehint.legacy.php
│ │ ├── required_and_optional_args.new.php
│ │ ├── vars_defined_elsewhere.legacy.php
│ │ ├── inline_html.new.php
│ │ ├── class_vars.new.php
│ │ ├── var_typehint.new.php
│ │ ├── kitchen_sink.legacy.php
│ │ ├── vars_defined_elsewhere.new.php
│ │ └── kitchen_sink.new.php
│ │ ├── RequestVariableCounter.php
│ │ ├── RequestClass.php
│ │ ├── UseStatement.php
│ │ └── RpcParameter.php
└── TestUtils
│ ├── GcloudWrapper.php
│ ├── PHPUnit4To6Shim.php
│ ├── FileUtil.php
│ ├── AppEngineDeploymentTrait.php
│ ├── CloudSqlProxyTrait.php
│ ├── ExponentialBackoffTrait.php
│ ├── DevAppserverTestTrait.php
│ ├── EventuallyConsistentTestTrait.php
│ ├── DeploymentTrait.php
│ ├── GcloudWrapper
│ └── GcloudWrapperTrait.php
│ ├── CloudFunctionLocalTestTrait.php
│ └── ExecuteCommandTrait.php
├── .github
├── CODEOWNERS
└── workflows
│ ├── static-analysis.yml
│ ├── release-checks.yml
│ ├── tests.yml
│ ├── code-standards.yml
│ └── doctum.yml
├── scripts
├── dump_credentials.php
├── php-tools
└── install_test_deps.sh
├── .php-cs-fixer.default.php
├── phpunit.xml.dist
├── composer.json
├── CONTRIBUTING.md
└── README.md
/test/fixtures/clonedir/file1:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/test/fixtures/clonedir/file2:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/test/fixtures/clonedir/file3:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/examples:
--------------------------------------------------------------------------------
1 | src/Fixers/ClientUpgradeFixer/examples/
--------------------------------------------------------------------------------
/test/fixtures/appengine-standard/app-php72.yaml.dist:
--------------------------------------------------------------------------------
1 | runtime: php72
2 |
--------------------------------------------------------------------------------
/test/src/snippet3.php:
--------------------------------------------------------------------------------
1 | listInfoTypes();
12 |
--------------------------------------------------------------------------------
/.github/CODEOWNERS:
--------------------------------------------------------------------------------
1 | # Code owners file.
2 | # This file controls who is tagged for review for any given pull request.
3 | #
4 | # For syntax help see:
5 | # https://help.github.com/en/github/creating-cloning-and-archiving-repositories/about-code-owners#codeowners-syntax
6 |
7 | * @GoogleCloudPlatform/php-admins @googleapis/yoshi-php
8 |
--------------------------------------------------------------------------------
/test/src/function_snippet_invalid.php:
--------------------------------------------------------------------------------
1 | listInfoTypes($listInfoTypesRequest);
14 |
--------------------------------------------------------------------------------
/scripts/dump_credentials.php:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env php
2 | dlpJobName('my-project', 'my-job');
13 |
14 | // Call the "close" method
15 | $job = $dlp->close();
16 |
17 | // Call an RPC method
18 | $job = $dlp->getDlpJob($jobName);
19 |
20 | // Call a non-existant method!
21 | $job = $dlp->getJob($jobName);
22 |
--------------------------------------------------------------------------------
/src/Fixers/ClientUpgradeFixer/examples/required_args.legacy.php:
--------------------------------------------------------------------------------
1 | createDlpJob('this/is/a/parent');
12 |
13 | // required args string (double quotes)
14 | $dlp->createDlpJob("this/is/a/$variable");
15 |
16 | // required args inline array
17 | $dlp->createDlpJob(['jobId' => 'abc', 'locationId' => 'def']);
18 |
19 | // required args variable
20 | $dlp->createDlpJob($foo);
21 |
--------------------------------------------------------------------------------
/test/fixtures/appengine-standard/composer.json:
--------------------------------------------------------------------------------
1 | {
2 | "require": {
3 | "silex/silex": "1.3 || ^2.3"
4 | },
5 | "require-dev": {
6 | "guzzlehttp/guzzle": "~5.3 || ~6.0",
7 | "symfony/browser-kit": "^3.4 || ^4.3",
8 | "symfony/process": "^3.4 || ^4.3 || ^5.0",
9 | "phpunit/phpunit": "^5"
10 | },
11 | "autoload": {
12 | "psr-4": {
13 | "Google\\Cloud\\TestUtils\\": "../../../src/TestUtils/",
14 | "Google\\Cloud\\Utils\\": "../../../src/Utils/",
15 | "Google\\Cloud\\Test\\": "tests/e2e"
16 | }
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/src/Utils/WordPress/files/php.ini:
--------------------------------------------------------------------------------
1 | ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
2 | ; Any PHP configuration in this file will be set in your App engine production ;
3 | ; instances. ;
4 | ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
5 |
6 | ; This PHP configuration is required by WordPress
7 | allow_url_include = "1"
8 |
9 | ; Configures WordPress to allow sufficiently large file uploads
10 | upload_max_filesize = 8M
11 |
12 | ; for debugging purposes only. Please remove for production instances
13 | display_errors=On
14 |
--------------------------------------------------------------------------------
/src/Fixers/ClientUpgradeFixer/examples/non_rpc_methods.new.php:
--------------------------------------------------------------------------------
1 | dlpJobName('my-project', 'my-job');
14 |
15 | // Call the "close" method
16 | $job = $dlp->close();
17 |
18 | // Call an RPC method
19 | $getDlpJobRequest = (new GetDlpJobRequest())
20 | ->setName($jobName);
21 | $job = $dlp->getDlpJob($getDlpJobRequest);
22 |
23 | // Call a non-existant method!
24 | $job = $dlp->getJob($jobName);
25 |
--------------------------------------------------------------------------------
/src/TestUtils/GcloudWrapper.php:
--------------------------------------------------------------------------------
1 | setRules($rules)
20 | ->setFinder(
21 | PhpCsFixer\Finder::create()
22 | ->in($configPath ?: __DIR__)
23 | ->notPath($excludePatterns)
24 | )
25 | ;
26 |
--------------------------------------------------------------------------------
/src/Fixers/ClientUpgradeFixer/RequestVariableCounter.php:
--------------------------------------------------------------------------------
1 | varCounts) == 1
12 | && array_values($this->varCounts)[0] == 1;
13 | }
14 |
15 | public function getNextVariableName(string $shortName): string
16 | {
17 | if (!isset($this->varCounts[$shortName])) {
18 | $this->varCounts[$shortName] = 0;
19 | }
20 | $num = (string) ++$this->varCounts[$shortName];
21 | // determine $request variable name depending on call count
22 | return sprintf(
23 | '$%s%s',
24 | lcfirst($shortName),
25 | $num == '1' ? '' : $num
26 | );
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/test/bootstrap.php:
--------------------------------------------------------------------------------
1 | get('/', function () use ($app) {
25 | return "Hello World!";
26 | });
27 |
28 | return $app;
29 |
--------------------------------------------------------------------------------
/.github/workflows/static-analysis.yml:
--------------------------------------------------------------------------------
1 | name: PHP Static Analysis
2 | on:
3 | workflow_call:
4 | inputs:
5 | version:
6 | type: string
7 | default: "^1.8"
8 | paths:
9 | type: string
10 | default: "src"
11 | autoload-file:
12 | type: string
13 | default: "vendor/autoload.php"
14 |
15 | permissions:
16 | contents: read
17 |
18 | jobs:
19 | static_analysis:
20 | runs-on: ubuntu-latest
21 | name: PHPStan Static Analysis
22 | steps:
23 | - uses: actions/checkout@v4
24 | - name: Setup PHP
25 | uses: shivammathur/setup-php@v2
26 | with:
27 | php-version: '8.4'
28 | - name: Run Script
29 | run: |
30 | composer install -q
31 | composer global require phpstan/phpstan:${{ inputs.version }} -q
32 | ~/.composer/vendor/bin/phpstan analyse ${{ inputs.paths }} \
33 | --autoload-file=${{ inputs.autoload-file }}
34 |
--------------------------------------------------------------------------------
/src/Fixers/ClientUpgradeFixer/examples/mixins.legacy.php:
--------------------------------------------------------------------------------
1 | getIamPolicy($resource);
12 |
13 | // IAM conditions need at least version 3
14 | if ($policy->getVersion() != 3) {
15 | $policy->setVersion(3);
16 | }
17 |
18 | $binding = new Binding([
19 | 'role' => 'roles/spanner.fineGrainedAccessUser',
20 | 'members' => [$iamMember],
21 | 'condition' => new Expr([
22 | 'title' => $title,
23 | 'expression' => sprintf("resource.name.endsWith('/databaseRoles/%s')", $databaseRole)
24 | ])
25 | ]);
26 | $policy->setBindings([$binding]);
27 | $secretManager->setIamPolicy($resource, $policy);
28 |
--------------------------------------------------------------------------------
/test/Utils/Flex/data/cloudsql-cloudbuild-yaml:
--------------------------------------------------------------------------------
1 | steps:
2 | - name: gcr.io/cloud-builders/docker
3 | args: ['pull', 'gcr.io/cloudsql-docker/gce-proxy:1.11']
4 | id: cloud-sql-proxy-pull
5 | - name: gcr.io/cloud-builders/docker
6 | args: ['run', '-d', '--network=cloudbuild', '-v', '/cloudsql:/cloudsql', 'gcr.io/cloudsql-docker/gce-proxy:1.11', '/cloud_sql_proxy', '-dir=/cloudsql', '-instances=my-project:us-central1:my-instance']
7 | wait_for: ['cloud-sql-proxy-pull']
8 | id: cloud-sql-proxy-run
9 | - name: gcr.io/cloud-builders/docker
10 | args: ['pull', 'us.gcr.io/my-project/appengine/default.my-version@sha256:sha256valuefortest']
11 | id: target-image-pull
12 | wait_for: ['cloud-sql-proxy-run']
13 | - name: gcr.io/cloud-builders/docker
14 | args: ['run', '-t', '--network=cloudbuild', '-v', '/cloudsql:/cloudsql', 'us.gcr.io/my-project/appengine/default.my-version@sha256:sha256valuefortest', 'ls','my dir']
15 | wait_for: ['target-image-pull']
16 | id: target-image-run
17 |
--------------------------------------------------------------------------------
/src/Fixers/ClientUpgradeFixer/examples/required_args.new.php:
--------------------------------------------------------------------------------
1 | setParent('this/is/a/parent');
14 | $dlp->createDlpJob($createDlpJobRequest);
15 |
16 | // required args string (double quotes)
17 | $createDlpJobRequest2 = (new CreateDlpJobRequest())
18 | ->setParent("this/is/a/$variable");
19 | $dlp->createDlpJob($createDlpJobRequest2);
20 |
21 | // required args inline array
22 | $createDlpJobRequest3 = (new CreateDlpJobRequest())
23 | ->setParent(['jobId' => 'abc', 'locationId' => 'def']);
24 | $dlp->createDlpJob($createDlpJobRequest3);
25 |
26 | // required args variable
27 | $createDlpJobRequest4 = (new CreateDlpJobRequest())
28 | ->setParent($foo);
29 | $dlp->createDlpJob($createDlpJobRequest4);
30 |
--------------------------------------------------------------------------------
/src/Fixers/ClientUpgradeFixer/examples/multiple_clients.legacy.php:
--------------------------------------------------------------------------------
1 | listInfoTypes();
24 | $secrets = $secretmanager->listSecrets('this/is/a/parent');
25 |
26 | // these shouldn't update
27 | $operations = $longrunning->listOperations();
28 | $serviceAccount = $storage->getServiceAccount();
29 |
--------------------------------------------------------------------------------
/test/fixtures/appengine-standard/index.php:
--------------------------------------------------------------------------------
1 | run();
29 |
--------------------------------------------------------------------------------
/src/Utils/WordPress/files/app.yaml:
--------------------------------------------------------------------------------
1 | # App Engine runtime configuration
2 | runtime: php72
3 |
4 | # Defaults to "serve index.php" and "serve public/index.php". Can be used to
5 | # serve a custom PHP front controller (e.g. "serve backend/index.php") or to
6 | # run a long-running PHP script as a worker process (e.g. "php worker.php").
7 | entrypoint: serve gae-app.php
8 |
9 | # Defines static handlers to serve WordPress assets
10 | handlers:
11 | - url: /(.*\.(htm|html|css|js))
12 | static_files: \1
13 | upload: .*\.(htm|html|css|js)$
14 |
15 | - url: /wp-content/(.*\.(ico|jpg|jpeg|png|gif|woff|ttf|otf|eot|svg))
16 | static_files: wp-content/\1
17 | upload: wp-content/.*\.(ico|jpg|jpeg|png|gif|woff|ttf|otf|eot|svg)$
18 |
19 | - url: /(.*\.(ico|jpg|jpeg|png|gif|woff|ttf|otf|eot|svg))
20 | static_files: \1
21 | upload: .*\.(ico|jpg|jpeg|png|gif|woff|ttf|otf|eot|svg)$
22 |
23 | - url: /wp-includes/images/media/(.*\.(ico|jpg|jpeg|png|gif|woff|ttf|otf|eot|svg))
24 | static_files: wp-includes/images/media/\1
25 | upload: wp-includes/images/media/.*\.(ico|jpg|jpeg|png|gif|woff|ttf|otf|eot|svg)$
26 |
--------------------------------------------------------------------------------
/test/TestUtils/command.php:
--------------------------------------------------------------------------------
1 | add(new Command('test'))
13 | ->addArgument('foo', InputArgument::OPTIONAL, 'fake argument')
14 | ->addOption('bar', '', InputOption::VALUE_REQUIRED, 'fake option')
15 | ->addOption('exception', '', InputOption::VALUE_NONE, 'throw an exception once')
16 | ->setCode(function ($input, $output) {
17 | printf('foo: %s, bar: %s',
18 | $input->getArgument('foo'),
19 | $input->getOption('bar'));
20 |
21 | if ($input->getOption('exception')) {
22 | // Increment call count so we know how many times we've retried
23 | ExecuteCommandTraitTest::incrementCallCount();
24 | throw new Exception('Threw an exception!');
25 | }
26 | });
27 |
28 | return $application;
29 |
--------------------------------------------------------------------------------
/src/Fixers/ClientUpgradeFixer/examples/vars_in_constructor.legacy.php:
--------------------------------------------------------------------------------
1 | dlp->listInfoTypes();
22 | }
23 |
24 | public function callSecretManager()
25 | {
26 | $secrets = $this->secretmanager->listSecrets('this/is/a/parent');
27 | }
28 |
29 | public function callStatic()
30 | {
31 | // These shouldn't update
32 | $secrets = self::$dlp->listInfoTypes(); // @phpstan-ignore-line
33 | $secrets = self::$secretmanager->listSecrets('this/is/a/parent'); // @phpstan-ignore-line
34 |
35 | // This should
36 | $secrets = self::$staticDlp->listInfoTypes();
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/test/fixtures/appengine-standard/phpunit.xml.dist:
--------------------------------------------------------------------------------
1 |
2 |
17 |
18 |
19 |
20 | tests
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 | app.php
29 |
30 |
31 |
32 |
--------------------------------------------------------------------------------
/src/Fixers/ClientUpgradeFixer/examples/optional_args.legacy.php:
--------------------------------------------------------------------------------
1 | listInfoTypes($parent);
16 |
17 | // optional args array (inline array)
18 | $job = $dlp->createDlpJob($parent, ['jobId' => 'abc', 'locationId' => 'def']);
19 |
20 | // optional args array (inline with nested arrays)
21 | $job = $dlp->createDlpJob($parent, [
22 | 'inspectJob' => new InspectJobConfig([
23 | 'inspect_config' => (new InspectConfig())
24 | ->setMinLikelihood(likelihood::LIKELIHOOD_UNSPECIFIED)
25 | ->setLimits($limits)
26 | ->setInfoTypes($infoTypes)
27 | ->setIncludeQuote(true),
28 | 'storage_config' => (new StorageConfig())
29 | ->setCloudStorageOptions(($cloudStorageOptions))
30 | ->setTimespanConfig($timespanConfig),
31 | ])
32 | ]);
33 |
--------------------------------------------------------------------------------
/src/Fixers/ClientUpgradeFixer/examples/optional_args_array_keyword.legacy.php:
--------------------------------------------------------------------------------
1 | listInfoTypes($args);
16 |
17 | // optional args array (inline array)
18 | $job = $dlp->createDlpJob($parent, array('jobId' => 'abc', 'locationId' => 'def'));
19 |
20 | // optional args array (inline with nested arrays)
21 | $job = $dlp->createDlpJob($parent, array(
22 | 'inspectJob' => new InspectJobConfig(array(
23 | 'inspect_config' => (new InspectConfig())
24 | ->setMinLikelihood(likelihood::LIKELIHOOD_UNSPECIFIED)
25 | ->setLimits($limits)
26 | ->setInfoTypes($infoTypes)
27 | ->setIncludeQuote(true),
28 | 'storage_config' => (new StorageConfig())
29 | ->setCloudStorageOptions(($cloudStorageOptions))
30 | ->setTimespanConfig($timespanConfig),
31 | ))
32 | ));
33 |
--------------------------------------------------------------------------------
/test/Utils/Flex/data/basic-describe-result:
--------------------------------------------------------------------------------
1 | {
2 | "automaticScaling": {
3 | "coolDownPeriod": "120s",
4 | "cpuUtilization": {
5 | "targetUtilization": 0.5
6 | },
7 | "maxTotalInstances": 20,
8 | "minTotalInstances": 2
9 | },
10 | "betaSettings": {
11 | "has_docker_image": "true",
12 | "module_yaml_path": "app.yaml",
13 | "no_appserver_affinity": "true",
14 | "use_deployment_manager": "true"
15 | },
16 | "createTime": "2017-10-10T18:04:31Z",
17 | "createdBy": "tmatsuo@google.com",
18 | "deployment": {
19 | "container": {
20 | "image": "us.gcr.io/my-project/appengine/default.my-version@sha256:sha256valuefortest"
21 | }
22 | },
23 | "env": "flexible",
24 | "handlers": [
25 | {
26 | "authFailAction": "AUTH_FAIL_ACTION_REDIRECT",
27 | "login": "LOGIN_OPTIONAL",
28 | "script": {
29 | "scriptPath": "PLACEHOLDER"
30 | },
31 | "securityLevel": "SECURE_OPTIONAL",
32 | "urlRegex": ".*"
33 | }
34 | ],
35 | "id": "my-version",
36 | "name": "apps/my-project/services/default/versions/my-version",
37 | "runtime": "php",
38 | "servingStatus": "SERVING",
39 | "threadsafe": true,
40 | "versionUrl": "https://my-version-dot-my-project.appspot.com"
41 | }
42 |
--------------------------------------------------------------------------------
/src/Fixers/ClientUpgradeFixer/examples/optional_args_variable.legacy.php:
--------------------------------------------------------------------------------
1 | listInfoTypes($parent);
16 |
17 | // optional args array (inline array)
18 | $options = ['jobId' => 'abc', 'locationId' => 'def'];
19 | $job = $dlp->createDlpJob($parent, $options);
20 |
21 | // optional args array (inline with nested arrays)
22 | $options2 = [
23 | 'inspectJob' => new InspectJobConfig([
24 | 'inspect_config' => (new InspectConfig())
25 | ->setMinLikelihood(likelihood::LIKELIHOOD_UNSPECIFIED)
26 | ->setLimits($limits)
27 | ->setInfoTypes($infoTypes)
28 | ->setIncludeQuote(true),
29 | 'storage_config' => (new StorageConfig())
30 | ->setCloudStorageOptions(($cloudStorageOptions))
31 | ->setTimespanConfig($timespanConfig),
32 | ])
33 | ];
34 | $job = $dlp->createDlpJob($parent, $options2);
35 |
--------------------------------------------------------------------------------
/scripts/php-tools:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env php
2 | add(new RunCsFixerCommand());
32 | $app->run();
33 |
--------------------------------------------------------------------------------
/src/Fixers/ClientUpgradeFixer/examples/mixins.new.php:
--------------------------------------------------------------------------------
1 | setResource($resource);
15 | $policy = $secretManager->getIamPolicy($getIamPolicyRequest);
16 |
17 | // IAM conditions need at least version 3
18 | if ($policy->getVersion() != 3) {
19 | $policy->setVersion(3);
20 | }
21 |
22 | $binding = new Binding([
23 | 'role' => 'roles/spanner.fineGrainedAccessUser',
24 | 'members' => [$iamMember],
25 | 'condition' => new Expr([
26 | 'title' => $title,
27 | 'expression' => sprintf("resource.name.endsWith('/databaseRoles/%s')", $databaseRole)
28 | ])
29 | ]);
30 | $policy->setBindings([$binding]);
31 | $setIamPolicyRequest = (new SetIamPolicyRequest())
32 | ->setResource($resource)
33 | ->setPolicy($policy);
34 | $secretManager->setIamPolicy($setIamPolicyRequest);
35 |
--------------------------------------------------------------------------------
/src/TestUtils/PHPUnit4To6Shim.php:
--------------------------------------------------------------------------------
1 | client->get('');
26 | } catch (\GuzzleHttp\Exception\ServerException $e) {
27 | $this->fail($e->getResponse()->getBody());
28 | }
29 | $this->assertEquals('200', $resp->getStatusCode(),
30 | 'top page status code');
31 | $this->assertStringContainsString(
32 | 'Hello World',
33 | $resp->getBody()->getContents());
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/test/Utils/Flex/data/cloudsql-describe-result:
--------------------------------------------------------------------------------
1 | {
2 | "automaticScaling": {
3 | "coolDownPeriod": "120s",
4 | "cpuUtilization": {
5 | "targetUtilization": 0.5
6 | },
7 | "maxTotalInstances": 20,
8 | "minTotalInstances": 2
9 | },
10 | "betaSettings": {
11 | "cloud_sql_instances": "my-project:us-central1:my-instance",
12 | "has_docker_image": "true",
13 | "module_yaml_path": "app.yaml",
14 | "no_appserver_affinity": "true",
15 | "use_deployment_manager": "true"
16 | },
17 | "createTime": "2017-10-10T18:04:31Z",
18 | "createdBy": "tmatsuo@google.com",
19 | "deployment": {
20 | "container": {
21 | "image": "us.gcr.io/my-project/appengine/default.my-version@sha256:sha256valuefortest"
22 | }
23 | },
24 | "env": "flexible",
25 | "handlers": [
26 | {
27 | "authFailAction": "AUTH_FAIL_ACTION_REDIRECT",
28 | "login": "LOGIN_OPTIONAL",
29 | "script": {
30 | "scriptPath": "PLACEHOLDER"
31 | },
32 | "securityLevel": "SECURE_OPTIONAL",
33 | "urlRegex": ".*"
34 | }
35 | ],
36 | "id": "my-version",
37 | "name": "apps/my-project/services/default/versions/my-version",
38 | "runtime": "php",
39 | "servingStatus": "SERVING",
40 | "threadsafe": true,
41 | "versionUrl": "https://my-version-dot-my-project.appspot.com"
42 | }
43 |
--------------------------------------------------------------------------------
/src/Fixers/ClientUpgradeFixer/examples/multiple_clients.new.php:
--------------------------------------------------------------------------------
1 | listInfoTypes($listInfoTypesRequest);
27 | $listSecretsRequest = (new ListSecretsRequest())
28 | ->setParent('this/is/a/parent');
29 | $secrets = $secretmanager->listSecrets($listSecretsRequest);
30 |
31 | // these shouldn't update
32 | $operations = $longrunning->listOperations();
33 | $serviceAccount = $storage->getServiceAccount();
34 |
--------------------------------------------------------------------------------
/src/Utils/Flex/flex_exec:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env php
2 | add(new FlexExecCommand());
35 | $app->run();
36 |
--------------------------------------------------------------------------------
/phpunit.xml.dist:
--------------------------------------------------------------------------------
1 |
2 |
17 |
18 |
19 |
20 | test/Utils
21 | test/TestUtils
22 | test/Fixers
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 | src/*.php
31 |
32 |
33 |
34 |
35 |
36 |
37 |
--------------------------------------------------------------------------------
/src/Fixers/ClientUpgradeFixer/examples/required_and_optional_args.legacy.php:
--------------------------------------------------------------------------------
1 | createDlpJob($parent, $optionalArgs);
16 |
17 | // required args variable and optional args array
18 | $dlp->createDlpJob($parent, ['jobId' => 'abc', 'locationId' => 'def']);
19 |
20 | // required args string and optional variable
21 | $dlp->createDlpJob('path/to/parent', ['jobId' => 'abc', 'locationId' => 'def']);
22 |
23 | // required args variable and optional args array with nested array
24 | $job = $dlp->createDlpJob($parent, [
25 | 'inspectJob' => new InspectJobConfig([
26 | 'inspect_config' => (new InspectConfig())
27 | ->setMinLikelihood(likelihood::LIKELIHOOD_UNSPECIFIED)
28 | ->setLimits($limits)
29 | ->setInfoTypes($infoTypes)
30 | ->setIncludeQuote(true),
31 | 'storage_config' => (new StorageConfig())
32 | ->setCloudStorageOptions(($cloudStorageOptions))
33 | ->setTimespanConfig($timespanConfig),
34 | ])
35 | ]);
36 |
--------------------------------------------------------------------------------
/test/Utils/mocked_exec.php:
--------------------------------------------------------------------------------
1 | createClient();
35 |
36 | $crawler = $client->request('GET', '/');
37 |
38 | $this->assertTrue($client->getResponse()->isOk());
39 | $this->assertStringContainsString(
40 | 'Hello World',
41 | $client->getResponse()->getContent());
42 | }
43 | }
44 |
--------------------------------------------------------------------------------
/src/Fixers/ClientUpgradeFixer/examples/class_vars.legacy.php:
--------------------------------------------------------------------------------
1 | dlp = new DlpServiceClient();
24 | $this->secretmanager = new SecretManagerServiceClient();
25 | }
26 |
27 | public function callDlp()
28 | {
29 | $infoTypes = $this->dlp->listInfoTypes();
30 | }
31 |
32 | public function callSecretManager()
33 | {
34 | $secrets = $this->secretmanager->listSecrets('this/is/a/parent');
35 | }
36 |
37 | public function callDlpFromFunction(DlpServiceClient $client)
38 | {
39 | $infoTypes = $client->listInfoTypes();
40 | }
41 | }
42 |
43 | // Instantiate a wrapping object.
44 | $wrapper = new ClientWrapper();
45 |
46 | // these should update
47 | $infoTypes = $wrapper->dlp->listInfoTypes();
48 | $secrets = $wrapper->secretmanager->listSecrets('this/is/a/parent');
49 |
--------------------------------------------------------------------------------
/src/Fixers/ClientUpgradeFixer/examples/vars_in_constructor.new.php:
--------------------------------------------------------------------------------
1 | dlp->listInfoTypes($listInfoTypesRequest);
25 | }
26 |
27 | public function callSecretManager()
28 | {
29 | $listSecretsRequest = (new ListSecretsRequest())
30 | ->setParent('this/is/a/parent');
31 | $secrets = $this->secretmanager->listSecrets($listSecretsRequest);
32 | }
33 |
34 | public function callStatic()
35 | {
36 | // These shouldn't update
37 | $secrets = self::$dlp->listInfoTypes(); // @phpstan-ignore-line
38 | $secrets = self::$secretmanager->listSecrets('this/is/a/parent'); // @phpstan-ignore-line
39 |
40 | // This should
41 | $listInfoTypesRequest2 = new ListInfoTypesRequest();
42 | $secrets = self::$staticDlp->listInfoTypes($listInfoTypesRequest2);
43 | }
44 | }
45 |
--------------------------------------------------------------------------------
/test/fixtures/appengine-standard/tests/e2e/DeployTest.php:
--------------------------------------------------------------------------------
1 | setDir(realpath(__DIR__ . '/../..'));
37 | }
38 |
39 | /**
40 | * Called after deploying the app.
41 | */
42 | private static function afterDeploy()
43 | {
44 | // Delete app.yaml.
45 | unlink('app.yaml');
46 | }
47 | }
48 |
--------------------------------------------------------------------------------
/src/Fixers/ClientUpgradeFixer/examples/optional_args.new.php:
--------------------------------------------------------------------------------
1 | listInfoTypes($listInfoTypesRequest);
19 |
20 | // optional args array (inline array)
21 | $createDlpJobRequest = (new CreateDlpJobRequest())
22 | ->setParent($parent)
23 | ->setJobId('abc')
24 | ->setLocationId('def');
25 | $job = $dlp->createDlpJob($createDlpJobRequest);
26 |
27 | // optional args array (inline with nested arrays)
28 | $createDlpJobRequest2 = (new CreateDlpJobRequest())
29 | ->setParent($parent)
30 | ->setInspectJob(new InspectJobConfig([
31 | 'inspect_config' => (new InspectConfig())
32 | ->setMinLikelihood(likelihood::LIKELIHOOD_UNSPECIFIED)
33 | ->setLimits($limits)
34 | ->setInfoTypes($infoTypes)
35 | ->setIncludeQuote(true),
36 | 'storage_config' => (new StorageConfig())
37 | ->setCloudStorageOptions(($cloudStorageOptions))
38 | ->setTimespanConfig($timespanConfig),
39 | ]));
40 | $job = $dlp->createDlpJob($createDlpJobRequest2);
41 |
--------------------------------------------------------------------------------
/src/Fixers/ClientUpgradeFixer/examples/optional_args_array_keyword.new.php:
--------------------------------------------------------------------------------
1 | listInfoTypes($listInfoTypesRequest);
19 |
20 | // optional args array (inline array)
21 | $createDlpJobRequest = (new CreateDlpJobRequest())
22 | ->setParent($parent)
23 | ->setJobId('abc')
24 | ->setLocationId('def');
25 | $job = $dlp->createDlpJob($createDlpJobRequest);
26 |
27 | // optional args array (inline with nested arrays)
28 | $createDlpJobRequest2 = (new CreateDlpJobRequest())
29 | ->setParent($parent)
30 | ->setInspectJob(new InspectJobConfig(array(
31 | 'inspect_config' => (new InspectConfig())
32 | ->setMinLikelihood(likelihood::LIKELIHOOD_UNSPECIFIED)
33 | ->setLimits($limits)
34 | ->setInfoTypes($infoTypes)
35 | ->setIncludeQuote(true),
36 | 'storage_config' => (new StorageConfig())
37 | ->setCloudStorageOptions(($cloudStorageOptions))
38 | ->setTimespanConfig($timespanConfig),
39 | )));
40 | $job = $dlp->createDlpJob($createDlpJobRequest2);
41 |
--------------------------------------------------------------------------------
/src/Fixers/ClientUpgradeFixer/RequestClass.php:
--------------------------------------------------------------------------------
1 | reflection = new ReflectionClass($className);
15 | }
16 |
17 | public function getShortName(): string
18 | {
19 | return $this->reflection->getShortName();
20 | }
21 |
22 | public function getName(): string
23 | {
24 | return $this->reflection->getName();
25 | }
26 |
27 | public function getImportTokens(): array
28 | {
29 | return array_merge(
30 | [new Token([T_WHITESPACE, PHP_EOL])],
31 | UseStatement::getTokensFromClassName($this->getName())
32 | );
33 | }
34 |
35 | public function getInitTokens(string $requestVarName, bool $parenthesis)
36 | {
37 | // Add the code for creating the $request variable
38 | return array_filter([
39 | new Token([T_VARIABLE, $requestVarName]),
40 | new Token([T_WHITESPACE, ' ']),
41 | new Token('='),
42 | new Token([T_WHITESPACE, ' ']),
43 | $parenthesis ? new Token('(') : null,
44 | new Token([T_NEW, 'new']),
45 | new Token([T_WHITESPACE, ' ']),
46 | new Token([T_STRING, $this->getShortName()]),
47 | new Token('('),
48 | new Token(')'),
49 | $parenthesis ? new Token(')') : null,
50 | ]);
51 | }
52 | }
53 |
--------------------------------------------------------------------------------
/src/Fixers/ClientUpgradeFixer/examples/inline_html.legacy.php:
--------------------------------------------------------------------------------
1 | serializeToJsonString(), true),
23 | JSON_PRETTY_PRINT
24 | );
25 | }
26 | ?>
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
List Secrets
35 |
36 | listSecrets($parent) as $secret): ?>
37 |
= print_message($secret) ?>
38 |
39 |
40 |
41 |
List DLP Jobs
42 |
43 | listDlpJobs($parent) as $job): ?>
44 |
= print_message($job) ?>
45 |
46 |
47 |
48 |
49 |
50 |
--------------------------------------------------------------------------------
/.github/workflows/release-checks.yml:
--------------------------------------------------------------------------------
1 | name: Release Checks
2 | on:
3 | workflow_call:
4 | inputs:
5 | next-release-label-check:
6 | type: boolean
7 | default: false
8 | pull_request:
9 |
10 | permissions:
11 | contents: read
12 |
13 | jobs:
14 | next-release-label-check:
15 | name: Check for "next release" label
16 | runs-on: ubuntu-latest
17 | if: inputs.next-release-label-check
18 | steps:
19 | -
20 | name: Check for "next release" label
21 | uses: actions/github-script@v6
22 | with:
23 | script: |
24 | const { data: pulls } = await github.rest.pulls.list({
25 | owner: context.repo.owner,
26 | repo: context.repo.repo,
27 | state: 'open',
28 | });
29 |
30 | // check for open PRs which contain the 'next release' label
31 | const openPRs = pulls.filter(pr =>
32 | pr.labels.some(label => label.name === 'next release')
33 | ).map(pr => ` - #${pr.number}: ${pr.title}`);
34 |
35 | if (openPRs.length > 0) {
36 | const errorMessage = 'Found "next release" label on the following open pull requests:\n'
37 | + openPRs.join('\n')
38 | + '\nPlease merge them before release.';
39 | core.setFailed(errorMessage);
40 | } else {
41 | console.log('No "next release" label found on any open pull requests!');
42 | }
43 |
--------------------------------------------------------------------------------
/src/Fixers/ClientUpgradeFixer/examples/optional_args_variable.new.php:
--------------------------------------------------------------------------------
1 | listInfoTypes($listInfoTypesRequest);
19 |
20 | // optional args array (inline array)
21 | $options = ['jobId' => 'abc', 'locationId' => 'def'];
22 | $createDlpJobRequest = (new CreateDlpJobRequest())
23 | ->setParent($parent)
24 | ->setJobId($options['jobId'])
25 | ->setLocationId($options['locationId']);
26 | $job = $dlp->createDlpJob($createDlpJobRequest);
27 |
28 | // optional args array (inline with nested arrays)
29 | $options2 = [
30 | 'inspectJob' => new InspectJobConfig([
31 | 'inspect_config' => (new InspectConfig())
32 | ->setMinLikelihood(likelihood::LIKELIHOOD_UNSPECIFIED)
33 | ->setLimits($limits)
34 | ->setInfoTypes($infoTypes)
35 | ->setIncludeQuote(true),
36 | 'storage_config' => (new StorageConfig())
37 | ->setCloudStorageOptions(($cloudStorageOptions))
38 | ->setTimespanConfig($timespanConfig),
39 | ])
40 | ];
41 | $createDlpJobRequest2 = (new CreateDlpJobRequest())
42 | ->setParent($parent)
43 | ->setInspectJob($options2['inspectJob']);
44 | $job = $dlp->createDlpJob($createDlpJobRequest2);
45 |
--------------------------------------------------------------------------------
/composer.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "google/cloud-tools",
3 | "description": "PHP tools for Google Cloud Platform",
4 | "keywords": ["test", "appengine", "gcp"],
5 | "homepage": "https://github.com/GoogleCloudPlatform/php-tools",
6 | "type": "library",
7 | "license": "Apache-2.0",
8 | "authors": [
9 | {
10 | "name": "Takashi Matsuo",
11 | "email": "tmatsuo@google.com",
12 | "homepage": "https://wp.gaeflex.ninja/"
13 | }
14 | ],
15 | "require": {
16 | "php": ">=8.0",
17 | "guzzlehttp/guzzle": "~7.0",
18 | "symfony/browser-kit": "^5.0 || ^6.4 || ^7.0",
19 | "symfony/console": "^5.0 || ^6.4 || ^7.0",
20 | "symfony/filesystem": "^5.0 || ^6.4 || ^7.0",
21 | "symfony/process": "^5.0 || ^6.4 || ^7.0",
22 | "symfony/yaml": "^5.0 || ^6.4 || ^7.0",
23 | "twig/twig": "~3.0"
24 | },
25 | "bin": [
26 | "src/Utils/Flex/flex_exec",
27 | "src/Utils/WordPress/wp-gae",
28 | "scripts/dump_credentials.php",
29 | "scripts/install_test_deps.sh",
30 | "scripts/php-tools"
31 | ],
32 | "autoload": {
33 | "psr-4": {
34 | "Google\\Cloud\\Fixers\\": "src/Fixers/",
35 | "Google\\Cloud\\TestUtils\\": "src/TestUtils/",
36 | "Google\\Cloud\\Utils\\": "src/Utils/"
37 | }
38 | },
39 | "require-dev": {
40 | "google/cloud-core": "^1.20",
41 | "google/gax": "^1.0.0",
42 | "paragonie/random_compat": ">=2",
43 | "phpunit/phpunit": "^9",
44 | "phpspec/prophecy-phpunit": "^2.0",
45 | "friendsofphp/php-cs-fixer": "^3.62",
46 | "google/cloud-dlp": "^1.10",
47 | "google/cloud-storage": "^1.33",
48 | "google/cloud-secret-manager": "^1.12"
49 | }
50 | }
51 |
--------------------------------------------------------------------------------
/CONTRIBUTING.md:
--------------------------------------------------------------------------------
1 | # How to become a contributor and submit your own code
2 |
3 | ## Contributor License Agreements
4 |
5 | We'd love to accept your patches! Before we can take them, we
6 | have to jump a couple of legal hurdles.
7 |
8 | Please fill out either the individual or corporate Contributor License Agreement
9 | (CLA).
10 |
11 | * If you are an individual writing original source code and you're sure you
12 | own the intellectual property, then you'll need to sign an [individual CLA]
13 | (https://developers.google.com/open-source/cla/individual).
14 | * If you work for a company that wants to allow you to contribute your work,
15 | then you'll need to sign a [corporate CLA]
16 | (https://developers.google.com/open-source/cla/corporate).
17 |
18 | Follow either of the two links above to access the appropriate CLA and
19 | instructions for how to sign and return it. Once we receive it, we'll be able to
20 | accept your pull requests.
21 |
22 | ## Contributing A Patch
23 |
24 | 1. Submit an issue describing your proposed change to the repo in question.
25 | 1. The repo owner will respond to your issue promptly.
26 | 1. If your proposed change is accepted, and you haven't already done so, sign a
27 | Contributor License Agreement (see details above).
28 | 1. Fork the desired repo, develop and test your code changes.
29 | 1. Ensure that your code adheres to the existing style in the sample to which
30 | you are contributing. Refer to the
31 | [Google Cloud Platform Samples Style Guide]
32 | (https://github.com/GoogleCloudPlatform/Template/wiki/style.html) for the
33 | recommended coding standards for this organization.
34 | 1. Ensure that your code has an appropriate set of unit tests which all pass.
35 | Set up [Travis](./TRAVIS.md) to run the unit tests on your fork.
36 | 1. Submit a pull request.
37 |
--------------------------------------------------------------------------------
/src/Fixers/ClientUpgradeFixer/examples/var_typehint.legacy.php:
--------------------------------------------------------------------------------
1 | listInfoTypes();
28 | $secrets = $secretmanager->listSecrets('this/is/a/parent');
29 |
30 | // these shouldn't update
31 | $operations = $longrunning->listOperations();
32 |
33 | function get_dlp_service_client()
34 | {
35 | return new DlpServiceClient();
36 | }
37 |
38 | function get_secretmanager_service_client()
39 | {
40 | return new SecretManagerServiceClient();
41 | }
42 |
43 | function get_operations_service_client()
44 | {
45 | return new DlpServiceClient();
46 | }
47 |
48 | class VariablesInsideClass
49 | {
50 | /** @var DlpServiceClient $dlp */
51 | private $dlp;
52 | private SecretManagerServiceClient $secretmanager;
53 |
54 | public function callDlp()
55 | {
56 | // These should update
57 | $infoTypes = $this->dlp->listInfoTypes();
58 | $secrets = $this->secretmanager->listSecrets('this/is/a/parent');
59 | }
60 | }
61 |
--------------------------------------------------------------------------------
/src/Fixers/ClientUpgradeFixer/UseStatement.php:
--------------------------------------------------------------------------------
1 | getFullName();
31 | $clientShortName = $useDeclaration->getShortName();
32 | if (
33 | 0 === strpos($clientClass, 'Google\\')
34 | && 'Client' === substr($clientShortName, -6)
35 | && false === strpos($clientClass, '\\Client\\')
36 | && class_exists($clientClass)
37 | ) {
38 | if (false !== strpos(get_parent_class($clientClass), '\Gapic\\')) {
39 | $clients[$clientClass] = $useDeclaration;
40 | }
41 | }
42 | }
43 | return $clients;
44 | }
45 |
46 | public static function getUseDeclarations(Tokens $tokens): array
47 | {
48 | return (new NamespaceUsesAnalyzer())->getDeclarationsFromTokens($tokens);
49 | }
50 | }
51 |
--------------------------------------------------------------------------------
/src/Fixers/ClientUpgradeFixer/examples/required_and_optional_args.new.php:
--------------------------------------------------------------------------------
1 | setParent($parent);
18 | $dlp->createDlpJob($createDlpJobRequest);
19 |
20 | // required args variable and optional args array
21 | $createDlpJobRequest2 = (new CreateDlpJobRequest())
22 | ->setParent($parent)
23 | ->setJobId('abc')
24 | ->setLocationId('def');
25 | $dlp->createDlpJob($createDlpJobRequest2);
26 |
27 | // required args string and optional variable
28 | $createDlpJobRequest3 = (new CreateDlpJobRequest())
29 | ->setParent('path/to/parent')
30 | ->setJobId('abc')
31 | ->setLocationId('def');
32 | $dlp->createDlpJob($createDlpJobRequest3);
33 |
34 | // required args variable and optional args array with nested array
35 | $createDlpJobRequest4 = (new CreateDlpJobRequest())
36 | ->setParent($parent)
37 | ->setInspectJob(new InspectJobConfig([
38 | 'inspect_config' => (new InspectConfig())
39 | ->setMinLikelihood(likelihood::LIKELIHOOD_UNSPECIFIED)
40 | ->setLimits($limits)
41 | ->setInfoTypes($infoTypes)
42 | ->setIncludeQuote(true),
43 | 'storage_config' => (new StorageConfig())
44 | ->setCloudStorageOptions(($cloudStorageOptions))
45 | ->setTimespanConfig($timespanConfig),
46 | ]));
47 | $job = $dlp->createDlpJob($createDlpJobRequest4);
48 |
--------------------------------------------------------------------------------
/src/Fixers/ClientUpgradeFixer/examples/vars_defined_elsewhere.legacy.php:
--------------------------------------------------------------------------------
1 | listInfoTypes();
13 | // this should also update (from config)
14 | $secrets = $secretmanagerFromConfig->listSecrets('this/is/a/parent');
15 |
16 | // these shouldn't update
17 | $operations = $longrunning->listOperations();
18 | $serviceAccount = $storage->getServiceAccount();
19 |
20 | class MyClass extends SomethingWhichDefinedAClient
21 | {
22 | public $parent;
23 |
24 | public function callTheDlpClient()
25 | {
26 | // These are updated from values in the "clientVars" confguration
27 | $this->dlpFromConfig->listInfoTypes();
28 | self::$dlpFromConfig->listInfoTypes(); // @phpstan-ignore-line
29 | }
30 |
31 | public function callTheDlpClientWithADifferentParent()
32 | {
33 | // these should not be updated
34 | $this->parent->dlpFromConfig->listInfoTypes();
35 | $this->parent::$dlpFromConfig->listInfoTypes();
36 | }
37 |
38 | public function callSecretManagerWithWildcardParent()
39 | {
40 | // These are updated from values in the "clientVars" confguration
41 | $this->secretManagerClientFromConfig->listSecrets();
42 | $this::$secretManagerClientFromConfig->listSecrets(); // @phpstan-ignore-line
43 | $this->parent->secretManagerClientFromConfig->listSecrets();
44 | $this->parent::$secretManagerClientFromConfig->listSecrets();
45 | }
46 | }
47 |
48 | class SomethingWhichDefinedAClient
49 | {
50 | public $dlpFromConfig;
51 | public $secretManagerClientFromConfig;
52 | }
53 |
--------------------------------------------------------------------------------
/src/Utils/WordPress/files/gae-app.php:
--------------------------------------------------------------------------------
1 | serializeToJsonString(), true),
25 | JSON_PRETTY_PRINT
26 | );
27 | }
28 |
29 | $listSecretsRequest = (new ListSecretsRequest())
30 | ->setParent($parent);
31 | $listDlpJobsRequest = (new ListDlpJobsRequest())
32 | ->setParent($parent);
33 | ?>
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
List Secrets
42 |
43 | listSecrets($listSecretsRequest) as $secret): ?>
44 |
= print_message($secret) ?>
45 |
46 |
47 |
48 |
List DLP Jobs
49 |
50 | listDlpJobs($listDlpJobsRequest) as $job): ?>
51 |
= print_message($job) ?>
52 |
53 |
54 |
55 |
56 |
57 |
--------------------------------------------------------------------------------
/src/Fixers/ClientUpgradeFixer/examples/class_vars.new.php:
--------------------------------------------------------------------------------
1 | dlp = new DlpServiceClient();
26 | $this->secretmanager = new SecretManagerServiceClient();
27 | }
28 |
29 | public function callDlp()
30 | {
31 | $listInfoTypesRequest = new ListInfoTypesRequest();
32 | $infoTypes = $this->dlp->listInfoTypes($listInfoTypesRequest);
33 | }
34 |
35 | public function callSecretManager()
36 | {
37 | $listSecretsRequest = (new ListSecretsRequest())
38 | ->setParent('this/is/a/parent');
39 | $secrets = $this->secretmanager->listSecrets($listSecretsRequest);
40 | }
41 |
42 | public function callDlpFromFunction(DlpServiceClient $client)
43 | {
44 | $listInfoTypesRequest2 = new ListInfoTypesRequest();
45 | $infoTypes = $client->listInfoTypes($listInfoTypesRequest2);
46 | }
47 | }
48 |
49 | // Instantiate a wrapping object.
50 | $wrapper = new ClientWrapper();
51 |
52 | // these should update
53 | $listInfoTypesRequest3 = new ListInfoTypesRequest();
54 | $infoTypes = $wrapper->dlp->listInfoTypes($listInfoTypesRequest3);
55 | $listSecretsRequest2 = (new ListSecretsRequest())
56 | ->setParent('this/is/a/parent');
57 | $secrets = $wrapper->secretmanager->listSecrets($listSecretsRequest2);
58 |
--------------------------------------------------------------------------------
/.github/workflows/tests.yml:
--------------------------------------------------------------------------------
1 | name: Test Suite
2 | on:
3 | push:
4 | branches:
5 | - main
6 | pull_request:
7 |
8 | jobs:
9 | test:
10 | runs-on: ${{matrix.operating-system}}
11 | strategy:
12 | matrix:
13 | operating-system: [ ubuntu-latest ]
14 | php: [ "8.0", "8.1", "8.2", "8.3", "8.4" ]
15 | name: PHP ${{matrix.php }} Unit Test
16 | steps:
17 | - uses: actions/checkout@v4
18 | - name: Setup PHP
19 | uses: shivammathur/setup-php@v2
20 | with:
21 | php-version: ${{ matrix.php }}
22 | extensions: zip
23 | - name: Install Dependencies
24 | uses: nick-invision/retry@v3
25 | with:
26 | timeout_minutes: 10
27 | max_attempts: 3
28 | command: composer install
29 | - name: Set Test Bin Dir
30 | run: echo "$GITHUB_WORKSPACE/test/bin" >> $GITHUB_PATH
31 | - name: Run Script
32 | run: vendor/bin/phpunit
33 |
34 | code-standards:
35 | uses: ./.github/workflows/code-standards.yml
36 | with:
37 | path: src
38 | config: .php-cs-fixer.default.php
39 | exclude-patterns: '["Fixers/ClientUpgradeFixer/examples"]'
40 |
41 | code-standards-with-config:
42 | uses: ./.github/workflows/code-standards.yml
43 | with:
44 | path: .
45 | config: .php-cs-fixer.default.php
46 | exclude-patterns: '["vendor", "test", "examples", "scripts"]'
47 |
48 | static-analysis:
49 | uses: ./.github/workflows/static-analysis.yml
50 |
51 | release_checks:
52 | uses: ./.github/workflows/release-checks.yml
53 | with:
54 | next-release-label-check: true
55 |
56 | build_docs:
57 | uses: ./.github/workflows/doctum.yml
58 | with:
59 | title: "Google Cloud PHP Tools"
60 | default_version: ${{ github.head_ref || github.ref_name }}
61 | dry_run: true
62 |
--------------------------------------------------------------------------------
/src/TestUtils/FileUtil.php:
--------------------------------------------------------------------------------
1 | advance();
54 | }
55 | }
56 | }
57 | closedir($dir);
58 | }
59 | }
60 |
--------------------------------------------------------------------------------
/src/TestUtils/AppEngineDeploymentTrait.php:
--------------------------------------------------------------------------------
1 | deploy();
37 | }
38 |
39 | /**
40 | * Deploy the application.
41 | * Override DeploymentTrait::deployApp to ensure $gcloudWrapper exists.
42 | *
43 | * @beforeClass
44 | */
45 | public static function deployApp()
46 | {
47 | self::$gcloudWrapper = new GcloudWrapper(
48 | self::requireEnv('GOOGLE_PROJECT_ID'),
49 | getenv('GOOGLE_VERSION_ID') ?: null
50 | );
51 | self::doDeployApp();
52 | }
53 |
54 | /**
55 | * Delete a deployed App Engine app.
56 | */
57 | private static function doDelete()
58 | {
59 | self::$gcloudWrapper->delete();
60 | }
61 |
62 | /**
63 | * Return the URI of the deployed App Engine app.
64 | */
65 | private function getBaseUri()
66 | {
67 | return self::$gcloudWrapper->getBaseUrl();
68 | }
69 | }
70 |
--------------------------------------------------------------------------------
/src/Utils/Gcloud.php:
--------------------------------------------------------------------------------
1 | $args
57 | * @return array [int, string] The shell return value and the command output
58 | */
59 | public function exec($args)
60 | {
61 | $cmd = 'gcloud ' . implode(' ', array_map('escapeshellarg', $args));
62 | exec($cmd, $output, $ret);
63 | return [$ret, $output];
64 | }
65 | }
66 |
--------------------------------------------------------------------------------
/src/Fixers/ClientUpgradeFixer/examples/var_typehint.new.php:
--------------------------------------------------------------------------------
1 | listInfoTypes($listInfoTypesRequest);
31 | $listSecretsRequest = (new ListSecretsRequest())
32 | ->setParent('this/is/a/parent');
33 | $secrets = $secretmanager->listSecrets($listSecretsRequest);
34 |
35 | // these shouldn't update
36 | $operations = $longrunning->listOperations();
37 |
38 | function get_dlp_service_client()
39 | {
40 | return new DlpServiceClient();
41 | }
42 |
43 | function get_secretmanager_service_client()
44 | {
45 | return new SecretManagerServiceClient();
46 | }
47 |
48 | function get_operations_service_client()
49 | {
50 | return new DlpServiceClient();
51 | }
52 |
53 | class VariablesInsideClass
54 | {
55 | /** @var DlpServiceClient $dlp */
56 | private $dlp;
57 | private SecretManagerServiceClient $secretmanager;
58 |
59 | public function callDlp()
60 | {
61 | // These should update
62 | $infoTypes = $this->dlp->listInfoTypes();
63 | $listSecretsRequest2 = (new ListSecretsRequest())
64 | ->setParent('this/is/a/parent');
65 | $secrets = $this->secretmanager->listSecrets($listSecretsRequest2);
66 | }
67 | }
68 |
--------------------------------------------------------------------------------
/test/TestUtils/ExecuteCommandTraitTest.php:
--------------------------------------------------------------------------------
1 | runCommand('test');
39 | $this->assertEquals("foo: , bar: ", $output);
40 |
41 | $output = $this->runCommand('test', ['foo' => 'yay', '--bar' => 'baz']);
42 | $this->assertEquals("foo: yay, bar: baz", $output);
43 | }
44 |
45 | public function testRunCommandWithoutBackoffThrowsException()
46 | {
47 | $this->expectException(\Exception::class);
48 | $this->runCommand('test', ['--exception' => true]);
49 | }
50 |
51 | /** @runInSeparateProcess */
52 | public function testRunCommandWithBackoff()
53 | {
54 | $this->useBackoff($retries = 5);
55 | self::setDelayFunction(function ($delay) {
56 | // do nothing!
57 | });
58 | try {
59 | $this->runCommand('test', ['--exception' => true]);
60 | } catch (\Exception $e) {
61 | }
62 | $this->assertEquals($retries + 1, self::$callCount);
63 | }
64 |
65 | public static function incrementCallCount()
66 | {
67 | self::$callCount++;
68 | }
69 | }
70 |
--------------------------------------------------------------------------------
/test/TestUtils/CloudSqlProxyTraitTest.php:
--------------------------------------------------------------------------------
1 | startCloudSqlProxy($connectionString, $socketDir);
41 | $this->assertNotNull(self::$cloudSqlProxyProcess);
42 | $this->assertTrue(self::$cloudSqlProxyProcess->isRunning());
43 | $this->stopCloudSqlProxy();
44 | $this->assertFalse(self::$cloudSqlProxyProcess->isRunning());
45 | }
46 |
47 | public function testInvalidSocketDirThrowsException()
48 | {
49 | $this->expectException(\Exception::class);
50 | $this->expectExceptionMessage('Unable to create socket dir /this/is/invalid');
51 | $connectionString = '';
52 | $socketDir = '/this/is/invalid';
53 | $this->startCloudSqlProxy($connectionString, $socketDir);
54 | }
55 |
56 | /**
57 | * @runInSeparateProcess
58 | */
59 | public function testFailedRunThrowsException()
60 | {
61 | $this->expectException(\Exception::class);
62 | $this->expectExceptionMessage('Failed to start cloud_sql_proxy');
63 | $connectionString = 'invalid';
64 | $socketDir = '/tmp/cloudsql';
65 | $this->startCloudSqlProxy($connectionString, $socketDir);
66 | }
67 | }
68 |
--------------------------------------------------------------------------------
/test/TestUtils/FileUtilTest.php:
--------------------------------------------------------------------------------
1 | assertFileExists($newDir);
36 | $this->assertFileExists($newDir . '/appengine-standard');
37 | foreach (['app.php', 'phpunit.xml.dist'] as $file) {
38 | $this->assertFileExists($newDir . '/appengine-standard/' . $file);
39 | }
40 | }
41 |
42 | public function testCloneDirectoryIntoTempWithProgress()
43 | {
44 | $output = new \Symfony\Component\Console\Output\BufferedOutput;
45 | $progress = new \Symfony\Component\Console\Helper\ProgressBar($output);
46 |
47 | $newDir = FileUtil::cloneDirectoryIntoTmp(__DIR__ . '/../fixtures/clonedir', $progress);
48 |
49 | $this->assertStringContainsString('1 [', $output->fetch());
50 | }
51 |
52 | public function testCloneIntoDirectoryWithExistingFile()
53 | {
54 | $tmpDir = sys_get_temp_dir() . '/test-' . FileUtil::randomName(8);
55 | mkdir($tmpDir);
56 | $testText = 'This is the existing app.php';
57 | file_put_contents($tmpDir . '/app.php', $testText);
58 | FileUtil::copyDir(
59 | __DIR__ . '/../fixtures/appengine-standard',
60 | $tmpDir
61 | );
62 |
63 | $this->assertNotEquals($testText, file_get_contents($tmpDir . '/app.php'));
64 | }
65 | }
66 |
--------------------------------------------------------------------------------
/src/Fixers/ClientUpgradeFixer/examples/kitchen_sink.legacy.php:
--------------------------------------------------------------------------------
1 | listInfoTypes();
22 |
23 | // optional args array (variable form)
24 | $dlp->listInfoTypes($foo);
25 |
26 | // required args variable
27 | $dlp->createDlpJob($foo);
28 |
29 | // required args string
30 | $dlp->createDlpJob('this/is/a/parent');
31 |
32 | // required args array
33 | $dlp->createDlpJob(['jobId' => 'abc', 'locationId' => 'def']);
34 |
35 | // required args variable and optional args array
36 | $dlp->createDlpJob($parent, ['jobId' => 'abc', 'locationId' => 'def']);
37 |
38 | // required args variable and optional args variable
39 | $dlp->createDlpJob($parent, $optionalArgs);
40 |
41 | // required args variable and optional args array with nested array
42 | $job = $dlp->createDlpJob($parent, [
43 | 'inspectJob' => new InspectJobConfig([
44 | 'inspect_config' => (new InspectConfig())
45 | ->setMinLikelihood(likelihood::LIKELIHOOD_UNSPECIFIED)
46 | ->setLimits($limits)
47 | ->setInfoTypes($infoTypes)
48 | ->setIncludeQuote(true),
49 | 'storage_config' => (new StorageConfig())
50 | ->setCloudStorageOptions(($cloudStorageOptions))
51 | ->setTimespanConfig($timespanConfig),
52 | ]),
53 | 'trailingComma' => true,
54 | ]);
55 |
56 | $projectId = 'my-project';
57 | $secretId = 'my-secret';
58 |
59 | // Create the Secret Manager client.
60 | $client = new SecretManagerServiceClient();
61 |
62 | // Build the parent name from the project.
63 | $parent = $client->projectName($projectId);
64 |
65 | // Create the parent secret.
66 | $secret = $client->createSecret($parent, $secretId,
67 | new Secret([
68 | 'replication' => new Replication([
69 | 'automatic' => new Automatic(),
70 | ]),
71 | ])
72 | );
73 |
--------------------------------------------------------------------------------
/test/Utils/WordPress/DeployTest.php:
--------------------------------------------------------------------------------
1 | $dir,
44 | '--project_id' => $projectId,
45 | '--db_instance' => $dbInstance,
46 | '--db_user' => $dbUser,
47 | '--db_password' => $dbPassword,
48 | '--db_name' => getenv('WORDPRESS_DB_NAME') ?: 'wordpress_php72',
49 | ]);
50 |
51 | self::$gcloudWrapper->setDir($dir);
52 | }
53 |
54 | public function testIndex()
55 | {
56 | // Access the blog top page
57 | $resp = $this->client->get('');
58 | $this->assertEquals('200', $resp->getStatusCode());
59 | $this->assertStringContainsString(
60 | 'It looks like your WordPress installation is running on App '
61 | . 'Engine for PHP 7.2!',
62 | $resp->getBody()->getContents()
63 | );
64 | }
65 | }
66 |
--------------------------------------------------------------------------------
/src/TestUtils/CloudSqlProxyTrait.php:
--------------------------------------------------------------------------------
1 | start();
47 | $process->waitUntil(function ($type, $buffer) {
48 | print($buffer);
49 | return str_contains($buffer, 'Ready for new connections');
50 | });
51 | if (!$process->isRunning()) {
52 | if ($output = $process->getOutput()) {
53 | print($output);
54 | }
55 | if ($errorOutput = $process->getErrorOutput()) {
56 | print($errorOutput);
57 | }
58 | throw new Exception('Failed to start cloud_sql_proxy');
59 | }
60 | return self::$cloudSqlProxyProcess = $process;
61 | }
62 |
63 | /**
64 | * @afterClass
65 | */
66 | public static function stopCloudSqlProxy(): void
67 | {
68 | if (self::$cloudSqlProxyProcess && self::$cloudSqlProxyProcess->isRunning()) {
69 | self::$cloudSqlProxyProcess->stop();
70 | }
71 | }
72 | }
73 |
--------------------------------------------------------------------------------
/src/Fixers/ClientUpgradeFixer/examples/vars_defined_elsewhere.new.php:
--------------------------------------------------------------------------------
1 | listInfoTypes($listInfoTypesRequest);
16 | // this should also update (from config)
17 | $listSecretsRequest = (new ListSecretsRequest())
18 | ->setParent('this/is/a/parent');
19 | $secrets = $secretmanagerFromConfig->listSecrets($listSecretsRequest);
20 |
21 | // these shouldn't update
22 | $operations = $longrunning->listOperations();
23 | $serviceAccount = $storage->getServiceAccount();
24 |
25 | class MyClass extends SomethingWhichDefinedAClient
26 | {
27 | public $parent;
28 |
29 | public function callTheDlpClient()
30 | {
31 | // These are updated from values in the "clientVars" confguration
32 | $listInfoTypesRequest2 = new ListInfoTypesRequest();
33 | $this->dlpFromConfig->listInfoTypes($listInfoTypesRequest2);
34 | $listInfoTypesRequest3 = new ListInfoTypesRequest();
35 | self::$dlpFromConfig->listInfoTypes($listInfoTypesRequest3); // @phpstan-ignore-line
36 | }
37 |
38 | public function callTheDlpClientWithADifferentParent()
39 | {
40 | // these should not be updated
41 | $this->parent->dlpFromConfig->listInfoTypes();
42 | $this->parent::$dlpFromConfig->listInfoTypes();
43 | }
44 |
45 | public function callSecretManagerWithWildcardParent()
46 | {
47 | // These are updated from values in the "clientVars" confguration
48 | $listSecretsRequest2 = new ListSecretsRequest();
49 | $this->secretManagerClientFromConfig->listSecrets($listSecretsRequest2);
50 | $listSecretsRequest3 = new ListSecretsRequest();
51 | $this::$secretManagerClientFromConfig->listSecrets($listSecretsRequest3); // @phpstan-ignore-line
52 | $listSecretsRequest4 = new ListSecretsRequest();
53 | $this->parent->secretManagerClientFromConfig->listSecrets($listSecretsRequest4);
54 | $listSecretsRequest5 = new ListSecretsRequest();
55 | $this->parent::$secretManagerClientFromConfig->listSecrets($listSecretsRequest5);
56 | }
57 | }
58 |
59 | class SomethingWhichDefinedAClient
60 | {
61 | public $dlpFromConfig;
62 | public $secretManagerClientFromConfig;
63 | }
64 |
--------------------------------------------------------------------------------
/src/TestUtils/ExponentialBackoffTrait.php:
--------------------------------------------------------------------------------
1 | getCode() == Code::RESOURCE_EXHAUSTED;
38 | });
39 | }
40 |
41 | private function useExpectationFailedBackoff($retries = null)
42 | {
43 | self::useBackoff($retries, function ($exception) {
44 | return $exception instanceof ExpectationFailedException;
45 | });
46 | }
47 |
48 | private function useDeadlineExceededBackoff($retries = null)
49 | {
50 | self::useBackoff($retries, function ($exception) {
51 | return $exception instanceof ApiException
52 | && $exception->getCode() == Code::DEADLINE_EXCEEDED;
53 | });
54 | }
55 |
56 | private function useBackoff($retries = null, ?callable $retryFunction = null)
57 | {
58 | $backoff = new ExponentialBackoff(
59 | $retries ?: $this->expontentialBackoffRetryCount,
60 | $retryFunction
61 | );
62 |
63 | self::$backoff
64 | ? self::$backoff->combine($backoff)
65 | : self::$backoff = $backoff;
66 | }
67 |
68 | private function setDelayFunction(callable $delayFunction)
69 | {
70 | if (is_null(self::$backoff)) {
71 | throw new \LogicException('You must set self::$backoff first');
72 | }
73 | self::$backoff->setDelayFunction($delayFunction);
74 | }
75 | }
76 |
--------------------------------------------------------------------------------
/scripts/install_test_deps.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 | # Copyright 2016 Google Inc.
3 | #
4 | # Licensed under the Apache License, Version 2.0 (the "License");
5 | # you may not use this file except in compliance with the License.
6 | # You may obtain a copy of the License at
7 | #
8 | # http://www.apache.org/licenses/LICENSE-2.0
9 | #
10 | # Unless required by applicable law or agreed to in writing, software
11 | # distributed under the License is distributed on an "AS IS" BASIS,
12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | # See the License for the specific language governing permissions and
14 | # limitations under the License.
15 |
16 | set -e
17 |
18 | install_gcloud()
19 | {
20 | # Install gcloud
21 | # You need to have ${HOME}/google-cloud-sdk/bin in your ${PATH}
22 | if [ ! -d ${HOME}/google-cloud-sdk ]; then
23 | wget \
24 | https://dl.google.com/dl/cloudsdk/release/google-cloud-sdk.tar.gz \
25 | --directory-prefix=${HOME}
26 | pushd "${HOME}"
27 | tar xzf google-cloud-sdk.tar.gz
28 | ./google-cloud-sdk/install.sh \
29 | --usage-reporting false \
30 | --path-update false \
31 | --command-completion false
32 | popd
33 | fi
34 | }
35 |
36 | configure_gcloud()
37 | {
38 | if [ -n "${CLOUDSDK_ACTIVE_CONFIG_NAME}" ]; then
39 | gcloud config configurations create ${CLOUDSDK_ACTIVE_CONFIG_NAME} \
40 | || /bin/true
41 | fi
42 | # Configure gcloud
43 | gcloud config set app/promote_by_default false
44 | gcloud config set disable_prompts true
45 | if [ -f ${GOOGLE_APPLICATION_CREDENTIALS} ]; then
46 | gcloud auth activate-service-account --key-file \
47 | "${GOOGLE_APPLICATION_CREDENTIALS}"
48 | fi
49 | if [ -n "${GOOGLE_CLOUD_PROJECT}" ]; then
50 | gcloud config set project ${GOOGLE_CLOUD_PROJECT}
51 | elif [ -n "${GOOGLE_PROJECT_ID}" ]; then
52 | gcloud config set project ${GOOGLE_PROJECT_ID}
53 | fi
54 | gcloud -q components install app-engine-python
55 | gcloud -q components update
56 | if [ -n "${GCLOUD_VERBOSITY}" ]; then
57 | gcloud -q config set verbosity ${GCLOUD_VERBOSITY}
58 | fi
59 | gcloud info
60 | }
61 |
62 | install_php_cs_fixer()
63 | {
64 | # Install PHP-cs-fixer
65 | if [ ! -f php-cs-fixer ]; then
66 | wget http://cs.sensiolabs.org/download/php-cs-fixer-v2.phar -O php-cs-fixer
67 | chmod a+x php-cs-fixer
68 | fi
69 | }
70 |
71 | while test $# -gt 0
72 | do
73 | case "$1" in
74 | --gcloud)
75 | install_gcloud
76 | configure_gcloud
77 | ;;
78 | --cs-fixer)
79 | install_php_cs_fixer
80 | ;;
81 | --*) echo "bad option $1"
82 | ;;
83 | esac
84 | shift
85 | done
86 |
--------------------------------------------------------------------------------
/test/Fixers/ClientUpgradeFixerTest.php:
--------------------------------------------------------------------------------
1 | fixer = new ClientUpgradeFixer();
22 | if ($config) {
23 | $this->fixer->configure($config);
24 | }
25 |
26 | $legacyFilepath = self::SAMPLES_DIR . $filename;
27 | $newFilepath = str_replace('legacy.', 'new.', $legacyFilepath);
28 | $tokens = Tokens::fromCode(file_get_contents($legacyFilepath));
29 | $fileInfo = new SplFileInfo($legacyFilepath);
30 | $this->fixer->fix($fileInfo, $tokens);
31 | $code = $tokens->generateCode();
32 | if (!file_exists($newFilepath) || file_get_contents($newFilepath) !== $code) {
33 | if (getenv('UPDATE_FIXTURES=1')) {
34 | file_put_contents($newFilepath, $code);
35 | $this->markTestIncomplete('Updated fixtures');
36 | }
37 | if (!file_exists($newFilepath)) {
38 | $this->fail('File does not exist');
39 | }
40 | }
41 | $this->assertStringEqualsFile($newFilepath, $code);
42 | }
43 |
44 | public static function provideLegacySamples()
45 | {
46 | $samples = array_map(
47 | fn ($file) => [basename($file)],
48 | array_filter(
49 | glob(self::SAMPLES_DIR . '*'),
50 | fn ($file) => '.legacy.php' === substr(basename($file), -11)
51 | )
52 | );
53 | $samples = array_combine(
54 | array_map(fn ($file) => substr($file[0], 0, -11), $samples),
55 | $samples
56 | );
57 |
58 | // add custom config for vars_defined_elsewhere samples
59 | $samples['vars_defined_elsewhere'][] = [
60 | 'clientVars' => [
61 | '$secretmanagerFromConfig' => 'Google\\Cloud\\SecretManager\\V1\\SecretManagerServiceClient',
62 | '$this->dlpFromConfig' => 'Google\\Cloud\\Dlp\\V2\\DlpServiceClient',
63 | 'self::$dlpFromConfig' => 'Google\\Cloud\\Dlp\\V2\\DlpServiceClient',
64 | 'secretManagerClientFromConfig' => 'Google\\Cloud\\SecretManager\\V1\\SecretManagerServiceClient',
65 | '$secretManagerClientFromConfig' => 'Google\\Cloud\\SecretManager\\V1\\SecretManagerServiceClient',
66 | ]
67 | ];
68 |
69 | return $samples;
70 | }
71 | }
72 |
--------------------------------------------------------------------------------
/src/TestUtils/DevAppserverTestTrait.php:
--------------------------------------------------------------------------------
1 | run($targets, $phpCgi) === false) {
74 | self::fail('dev_appserver failed');
75 | }
76 | static::afterRun();
77 | }
78 |
79 | /**
80 | * Stop the devserver.
81 | *
82 | * @afterClass
83 | */
84 | public static function stopServer()
85 | {
86 | self::$gcloudWrapper->stop();
87 | }
88 |
89 | /**
90 | * Set up the client
91 | *
92 | * @before
93 | */
94 | public function setUpClient()
95 | {
96 | $url = self::$gcloudWrapper->getLocalBaseUrl();
97 | $this->client = new Client(['base_uri' => $url]);
98 | }
99 | }
100 |
--------------------------------------------------------------------------------
/src/TestUtils/EventuallyConsistentTestTrait.php:
--------------------------------------------------------------------------------
1 | eventuallyConsistentRetryCount;
57 | }
58 | if (is_null($catchAllExceptions)) {
59 | $catchAllExceptions = $this->catchAllExceptions;
60 | }
61 | $attempts = 0;
62 | while ($attempts < $maxAttempts) {
63 | try {
64 | return $func();
65 | } catch (\PHPUnit\Framework\ExpectationFailedException $testException) {
66 | // do nothing
67 | } catch (\Exception $testException) {
68 | if (!$catchAllExceptions) {
69 | throw $testException;
70 | }
71 | }
72 | // Increment the number of attempts, and if we are going to attempt
73 | // again, run the sleep function.
74 | $attempts++;
75 | if ($attempts < $maxAttempts) {
76 | $this->retrySleepFunc($attempts);
77 | }
78 | }
79 | throw $testException;
80 | }
81 |
82 | private function retrySleepFunc($attempts)
83 | {
84 | sleep(pow(2, $attempts));
85 | }
86 | }
87 |
--------------------------------------------------------------------------------
/test/TestUtils/ExponentialBackoffTraitTest.php:
--------------------------------------------------------------------------------
1 | useResourceExhaustedBackoff($retries = 5);
38 | $this->runBackoff(function () use (&$timesCalled) {
39 | $timesCalled++;
40 | throw new ApiException('Test', Code::RESOURCE_EXHAUSTED, '');
41 | });
42 | $this->assertEquals($retries + 1, $timesCalled);
43 | }
44 |
45 | public function testExpectationFailedBackoff()
46 | {
47 | $this->useExpectationFailedBackoff($retries = 5);
48 | $this->runBackoff(function () use (&$timesCalled) {
49 | $timesCalled++;
50 | $this->assertTrue(false);
51 | });
52 | $this->assertEquals($retries + 1, $timesCalled);
53 | }
54 |
55 | public function testExpectationFailedBackoffReturnsValue()
56 | {
57 | $this->useExpectationFailedBackoff();
58 | $retVal = $this->runBackoff(function () {
59 | return 'foo';
60 | });
61 | $this->assertEquals('foo', $retVal);
62 | }
63 |
64 | public function testRetryCountInstanceVar()
65 | {
66 | $this->expontentialBackoffRetryCount = $retries = 10;
67 | $this->useExpectationFailedBackoff();
68 |
69 | $this->runBackoff(function () use (&$timesCalled) {
70 | $timesCalled++;
71 | $this->assertTrue(false);
72 | });
73 | $this->assertEquals($retries + 1, $timesCalled);
74 | }
75 |
76 | public function testDefaultBackoffCatchesAllExceptions()
77 | {
78 | $this->useBackoff($retries = 5);
79 | $this->runBackoff(function () use (&$timesCalled) {
80 | $timesCalled++;
81 | throw new \Exception('Something went wrong');
82 | });
83 | $this->assertEquals($retries + 1, $timesCalled);
84 | }
85 |
86 | private function runBackoff(callable $func)
87 | {
88 | self::setDelayFunction(function ($delay) {
89 | // do nothing!
90 | });
91 | try {
92 | return self::$backoff->execute($func);
93 | } catch (\Exception $e) {
94 | // var_dump($e->getMessage());
95 | }
96 | }
97 | }
98 |
--------------------------------------------------------------------------------
/test/TestUtils/GcloudWrapper/CloudRunTest.php:
--------------------------------------------------------------------------------
1 | getMockBuilder(CloudRun::class)
36 | ->setMethods(['execWithRetry'])
37 | ->setConstructorArgs(['project'])
38 | ->getMock();
39 | $deployCmd = 'gcloud beta run deploy default --image ' . self::$image
40 | . ' --region us-central1 --platform managed'
41 | . ' --project project';
42 | $deleteCmd = 'gcloud beta run services delete default'
43 | . ' --region us-central1 --platform managed'
44 | . ' --project project';
45 | $mockGcloudWrapper->expects($this->exactly(2))
46 | ->method('execWithRetry')
47 | ->withConsecutive(
48 | [$this->equalTo($deployCmd), $this->equalTo(3)],
49 | [$this->equalTo($deleteCmd), $this->equalTo(3)]
50 | )
51 | ->will($this->returnValue(false));
52 |
53 | $mockGcloudWrapper->deploy(self::$image);
54 |
55 | $mockGcloudWrapper->delete();
56 | }
57 |
58 | public function testDeployAndDeleteWithCustomArgs()
59 | {
60 | $mockGcloudWrapper = $this->getMockBuilder(CloudRun::class)
61 | ->setMethods(['execWithRetry'])
62 | ->setConstructorArgs(['project', [
63 | 'platform' => 'gke',
64 | 'region' => '',
65 | 'service' => 'foo',
66 | ]])
67 | ->getMock();
68 | $mockGcloudWrapper->method('execWithRetry')->willReturn(true);
69 | $deployCmd = 'gcloud beta run deploy foo --image ' . self::$image
70 | . ' --platform gke --project project';
71 | $deleteCmd = 'gcloud beta run services delete foo'
72 | . ' --platform gke --project project';
73 | $mockGcloudWrapper->expects($this->exactly(2))
74 | ->method('execWithRetry')
75 | ->withConsecutive(
76 | [$this->equalTo($deployCmd), $this->equalTo(4)],
77 | [$this->equalTo($deleteCmd), $this->equalTo(4)]
78 | );
79 |
80 | $mockGcloudWrapper->deploy(self::$image, ['retries' => 4]);
81 |
82 | $mockGcloudWrapper->delete(['retries' => 4]);
83 | }
84 | }
85 |
--------------------------------------------------------------------------------
/src/Fixers/ClientUpgradeFixer/examples/kitchen_sink.new.php:
--------------------------------------------------------------------------------
1 | listInfoTypes($listInfoTypesRequest);
26 |
27 | // optional args array (variable form)
28 | $listInfoTypesRequest2 = new ListInfoTypesRequest();
29 | $dlp->listInfoTypes($listInfoTypesRequest2);
30 |
31 | // required args variable
32 | $createDlpJobRequest = (new CreateDlpJobRequest())
33 | ->setParent($foo);
34 | $dlp->createDlpJob($createDlpJobRequest);
35 |
36 | // required args string
37 | $createDlpJobRequest2 = (new CreateDlpJobRequest())
38 | ->setParent('this/is/a/parent');
39 | $dlp->createDlpJob($createDlpJobRequest2);
40 |
41 | // required args array
42 | $createDlpJobRequest3 = (new CreateDlpJobRequest())
43 | ->setParent(['jobId' => 'abc', 'locationId' => 'def']);
44 | $dlp->createDlpJob($createDlpJobRequest3);
45 |
46 | // required args variable and optional args array
47 | $createDlpJobRequest4 = (new CreateDlpJobRequest())
48 | ->setParent($parent)
49 | ->setJobId('abc')
50 | ->setLocationId('def');
51 | $dlp->createDlpJob($createDlpJobRequest4);
52 |
53 | // required args variable and optional args variable
54 | $createDlpJobRequest5 = (new CreateDlpJobRequest())
55 | ->setParent($parent);
56 | $dlp->createDlpJob($createDlpJobRequest5);
57 |
58 | // required args variable and optional args array with nested array
59 | $createDlpJobRequest6 = (new CreateDlpJobRequest())
60 | ->setParent($parent)
61 | ->setInspectJob(new InspectJobConfig([
62 | 'inspect_config' => (new InspectConfig())
63 | ->setMinLikelihood(likelihood::LIKELIHOOD_UNSPECIFIED)
64 | ->setLimits($limits)
65 | ->setInfoTypes($infoTypes)
66 | ->setIncludeQuote(true),
67 | 'storage_config' => (new StorageConfig())
68 | ->setCloudStorageOptions(($cloudStorageOptions))
69 | ->setTimespanConfig($timespanConfig),
70 | ]))
71 | ->setTrailingComma(true);
72 | $job = $dlp->createDlpJob($createDlpJobRequest6);
73 |
74 | $projectId = 'my-project';
75 | $secretId = 'my-secret';
76 |
77 | // Create the Secret Manager client.
78 | $client = new SecretManagerServiceClient();
79 |
80 | // Build the parent name from the project.
81 | $parent = $client->projectName($projectId);
82 |
83 | // Create the parent secret.
84 | $createSecretRequest = (new CreateSecretRequest())
85 | ->setParent($parent)
86 | ->setSecretId($secretId)
87 | ->setSecret(new Secret([
88 | 'replication' => new Replication([
89 | 'automatic' => new Automatic(),
90 | ]),
91 | ]));
92 | $secret = $client->createSecret($createSecretRequest);
93 |
--------------------------------------------------------------------------------
/src/TestUtils/DeploymentTrait.php:
--------------------------------------------------------------------------------
1 | client = new Client(['base_uri' => self::getBaseUri()]);
111 | }
112 |
113 | private static function getBaseUri()
114 | {
115 | throw new \Exception('This method must be implemented in your test');
116 | }
117 | }
118 |
--------------------------------------------------------------------------------
/test/Utils/ProjectTest.php:
--------------------------------------------------------------------------------
1 | assertEquals(getcwd() . '/relative-path', $project->getDir());
36 | rmdir(getcwd() . '/relative-path');
37 |
38 | // using an existing directory
39 | $project = new Project($dir = sys_get_temp_dir());
40 | $this->assertEquals(realpath($dir), $project->getDir());
41 |
42 | // creating a directory
43 | $newDir = $dir . '/newdir' . rand();
44 | $project = new Project($newDir);
45 | $this->assertEquals(realpath($newDir), $project->getDir());
46 |
47 | $this->assertEquals(
48 | 'A directory ' . $newDir . ' was created.',
49 | $project->getInfo()[0]
50 | );
51 | }
52 |
53 | public function testDownloadArchive()
54 | {
55 | $project = new Project(sys_get_temp_dir() . '/project' . rand());
56 | $archiveUrl = 'https://github.com/GoogleCloudPlatform/google-cloud-php/archive/main.zip';
57 | $project->downloadArchive('Google Cloud client libraries', $archiveUrl);
58 | $this->assertTrue(file_exists(
59 | $project->getDir() . '/google-cloud-php-main/composer.json'));
60 | }
61 |
62 | public function testCopyFiles()
63 | {
64 | $contents = "This is a TEST_PARAMETER template";
65 | $fromPath = tempnam(sys_get_temp_dir(), 'template');
66 | $fromDir = dirname($fromPath);
67 | $fromFile = basename($fromPath);
68 | file_put_contents($fromPath, $contents);
69 |
70 | // copy file with no parameters
71 | $project = new Project(sys_get_temp_dir() . '/project' . rand());
72 | $project->copyFiles($fromDir, [$fromFile => '/']);
73 | $newContents = file_get_contents($project->getDir() . '/' . $fromFile);
74 | $this->assertEquals($newContents, $contents);
75 |
76 | // copy file using parameters
77 | $project = new Project($dir = sys_get_temp_dir());
78 | $project->copyFiles($fromDir, [$fromFile => '/'], [
79 | 'TEST_PARAMETER' => 'foo bar baz yip yip',
80 | ]);
81 | $newContents = file_get_contents($project->getDir() . '/' . $fromFile);
82 | $this->assertEquals($newContents, 'This is a foo bar baz yip yip template');
83 | }
84 |
85 | public function testAvailableDbRegions()
86 | {
87 | $project = new Project(__DIR__);
88 | $this->assertContains('us-central1', $project->getAvailableDbRegions());
89 | }
90 | }
91 |
--------------------------------------------------------------------------------
/src/Fixers/ClientUpgradeFixer/RpcParameter.php:
--------------------------------------------------------------------------------
1 | reflection = $reflection;
16 | }
17 |
18 | public function isOptionalArgs(): bool
19 | {
20 | return $this->reflection->getName() === 'optionalArgs';
21 | }
22 |
23 | public function getSetter(): string
24 | {
25 | return 'set' . ucfirst($this->reflection->getName());
26 | }
27 |
28 | public static function getRpcCallParameters(Tokens $tokens, int $startIndex)
29 | {
30 | $arguments = [];
31 | $nextIndex = $tokens->getNextMeaningfulToken($startIndex);
32 | $lastIndex = null;
33 | if ($tokens[$nextIndex]->getContent() == '(') {
34 | $startIndex = $nextIndex;
35 | $lastIndex = $tokens->findBlockEnd(Tokens::BLOCK_TYPE_PARENTHESIS_BRACE, $nextIndex);
36 | $nextArgumentEnd = self::getNextArgumentEnd($tokens, $nextIndex);
37 | while ($nextArgumentEnd != $nextIndex) {
38 | $argumentTokens = [];
39 | for ($i = $nextIndex + 1; $i <= $nextArgumentEnd; $i++) {
40 | $argumentTokens[] = $tokens[$i];
41 | }
42 |
43 | $arguments[$nextIndex] = $argumentTokens;
44 | $nextIndex = $tokens->getNextMeaningfulToken($nextArgumentEnd);
45 | $nextArgumentEnd = self::getNextArgumentEnd($tokens, $nextIndex);
46 | }
47 | }
48 |
49 | return [$arguments, $startIndex, $lastIndex];
50 | }
51 |
52 | private static function getNextArgumentEnd(Tokens $tokens, int $index): int
53 | {
54 | $nextIndex = $tokens->getNextMeaningfulToken($index);
55 | $nextToken = $tokens[$nextIndex];
56 |
57 | while ($nextToken->equalsAny([
58 | '$',
59 | '[',
60 | '(',
61 | [CT::T_ARRAY_INDEX_CURLY_BRACE_OPEN],
62 | [CT::T_ARRAY_SQUARE_BRACE_OPEN],
63 | [CT::T_DYNAMIC_PROP_BRACE_OPEN],
64 | [CT::T_DYNAMIC_VAR_BRACE_OPEN],
65 | [CT::T_NAMESPACE_OPERATOR],
66 | [T_NS_SEPARATOR],
67 | [T_STATIC],
68 | [T_STRING],
69 | [T_CONSTANT_ENCAPSED_STRING],
70 | [T_VARIABLE],
71 | [T_NEW],
72 | [T_ARRAY],
73 | ])) {
74 | $blockType = Tokens::detectBlockType($nextToken);
75 |
76 | if (null !== $blockType) {
77 | $nextIndex = $tokens->findBlockEnd($blockType['type'], $nextIndex);
78 | }
79 |
80 | $index = $nextIndex;
81 | $nextIndex = $tokens->getNextMeaningfulToken($nextIndex);
82 | $nextToken = $tokens[$nextIndex];
83 | }
84 |
85 | if ($nextToken->isGivenKind(T_OBJECT_OPERATOR)) {
86 | return self::getNextArgumentEnd($tokens, $nextIndex);
87 | }
88 |
89 | if ($nextToken->isGivenKind(T_PAAMAYIM_NEKUDOTAYIM)) {
90 | return self::getNextArgumentEnd($tokens, $tokens->getNextMeaningfulToken($nextIndex));
91 | }
92 |
93 | if ('"' === $nextToken->getContent()) {
94 | if ($endIndex = $tokens->getNextTokenOfKind($nextIndex + 1, ['"'])) {
95 | return $endIndex;
96 | }
97 | }
98 |
99 | return $index;
100 | }
101 | }
102 |
--------------------------------------------------------------------------------
/.github/workflows/code-standards.yml:
--------------------------------------------------------------------------------
1 | name: PHP Code Standards
2 |
3 | on:
4 | workflow_call:
5 | inputs:
6 | version:
7 | type: string
8 | default: "^3.0"
9 | config:
10 | type: string
11 | default: ""
12 | path:
13 | type: string
14 | default: "."
15 | rules:
16 | type: string
17 | default: |
18 | {
19 | "@PSR2": true,
20 | "array_syntax": {"syntax":"short"},
21 | "concat_space": {"spacing":"one"},
22 | "new_with_parentheses": true,
23 | "no_unused_imports": true,
24 | "ordered_imports": true,
25 | "return_type_declaration": {"space_before": "none"},
26 | "single_quote": true,
27 | "single_space_around_construct": true,
28 | "cast_spaces": true,
29 | "whitespace_after_comma_in_array": true,
30 | "no_whitespace_in_blank_line": true,
31 | "binary_operator_spaces": {"default": "at_least_single_space"},
32 | "no_extra_blank_lines": true,
33 | "nullable_type_declaration_for_default_null_value": true
34 | }
35 | add-rules:
36 | type: string
37 | default: "{}"
38 | exclude-patterns:
39 | type: string
40 | default: ""
41 |
42 | permissions:
43 | contents: read
44 |
45 | jobs:
46 | php_code_standards:
47 | runs-on: ubuntu-latest
48 | name: PHP Code Standards
49 | env:
50 | CONFIG: ${{ inputs.config }}
51 | CONFIG_PATH: ${{ inputs.path }}
52 | steps:
53 | - uses: actions/checkout@v4
54 | - name: Setup PHP
55 | uses: shivammathur/setup-php@v2
56 | with:
57 | php-version: '8.2'
58 | # install Google Cloud Tools if a config is provided which starts with "GoogleCloudPlatform/php-tools/"
59 | - if: ${{ startsWith(inputs.config, 'GoogleCloudPlatform/php-tools/') }}
60 | name: Install Google Cloud Tools
61 | run: |
62 | BRANCH=${CONFIG#GoogleCloudPlatform/php-tools/}
63 | composer global require google/cloud-tools:dev-${BRANCH#*@} -q
64 | echo "CONFIG=$HOME/.composer/vendor/google/cloud-tools/${BRANCH%@*}" >> $GITHUB_ENV
65 | - name: 'Setup jq'
66 | uses: dcarbone/install-jq-action@v2
67 | - name: Install PHP CS Fixer
68 | run: composer global require friendsofphp/php-cs-fixer:${{ inputs.version }}
69 | - name: Run PHP CS Fixer
70 | run: |
71 | # set environment variables in script
72 | export RULES=$(echo $'${{ inputs.rules }} ${{ inputs.add-rules }}'|tr -d '\n\t\r '|jq -s '.[0] * .[1]' -crM)
73 | export EXCLUDE_PATTERNS=$(echo $'${{ inputs.exclude-patterns }}'|tr -d '\n\t\r ')
74 |
75 | # use config path only if EXCLUDE_PATTERN is empty
76 | CMD_PATH=$([ "$EXCLUDE_PATTERNS" = "" ] && echo "$CONFIG_PATH" || echo "")
77 | CONFIG_OR_RULES=$([ ! -z "$CONFIG" ] && echo "--config=$CONFIG" || echo --rules=$RULES)
78 |
79 | # do not fail if php-cs-fixer fails (so we can print debugging info)
80 | set +e
81 |
82 | ~/.composer/vendor/bin/php-cs-fixer fix \
83 | $CMD_PATH \
84 | $CONFIG_OR_RULES \
85 | --dry-run --diff
86 |
87 | if [ "$?" -ne 0 ]; then
88 | echo "Run this script locally by executing the following command" \
89 | "from the root of your ${{ github.repository }} repo:"
90 | echo ""
91 | echo " composer global require google/cloud-tools"
92 | echo " ~/.composer/vendor/bin/php-tools cs-fixer ${{ github.repository }} --ref ${{ github.head_ref || github.ref_name }}"
93 | echo ""
94 | exit 1
95 | fi
96 |
--------------------------------------------------------------------------------
/test/Utils/ContainerExecTest.php:
--------------------------------------------------------------------------------
1 | gcloud = $this->prophesize(Gcloud::class);
45 | $this->fs = new Filesystem();
46 | $this->tempnam = tempnam(sys_get_temp_dir(), 'flex-exec-test');
47 | $this->workdir = $this->tempnam . '_workdir';
48 | try {
49 | $this->fs->mkdir($this->workdir);
50 | } catch (IOExceptionInterface $e) {
51 | $this->fail("Failed to crete a workdir: " . $e->getTraceAsString());
52 | }
53 | }
54 |
55 | public function tearDown(): void
56 | {
57 | unlink($this->tempnam);
58 | $this->fs->remove($this->workdir);
59 | }
60 |
61 | public function testContainerExecInit()
62 | {
63 | $image = 'gcr.io/my-project/my-image';
64 | $containerExec = new ContainerExec(
65 | $image,
66 | ['ls', '-al', 'my dir'],
67 | $this->workdir,
68 | '',
69 | $this->gcloud->reveal()
70 | );
71 | }
72 |
73 | public function testContainerExecNonDir()
74 | {
75 | $image = 'gcr.io/my-project/my-image';
76 | try {
77 | $containerExec = new ContainerExec(
78 | $image,
79 | ['ls', '-al', 'my dir'],
80 | $this->workdir . '-non-existing',
81 | '',
82 | $this->gcloud->reveal()
83 | );
84 | } catch (\InvalidArgumentException $e) {
85 | // $e here has the function scope, we can assert it later.
86 | }
87 | $this->assertNotNull($e);
88 | $this->assertEquals(
89 | $this->workdir . '-non-existing is not a directory',
90 | $e->getMessage()
91 | );
92 | }
93 |
94 | public function testContainerExecRun()
95 | {
96 | $image = 'gcr.io/my-project/my-image';
97 | $this->gcloud->exec(
98 | [
99 | 'builds',
100 | 'submit',
101 | "--config=$this->workdir/cloudbuild.yaml",
102 | "$this->workdir"
103 | ]
104 | )->shouldBeCalledTimes(1)->willReturn([0, ['output']]);
105 | $containerExec = new ContainerExec(
106 | $image,
107 | ['ls', '-al', 'my dir'],
108 | $this->workdir,
109 | '',
110 | $this->gcloud->reveal()
111 | );
112 | $containerExec->run();
113 | }
114 | }
115 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Utilities for Google Cloud Platform
2 |
3 | ## Install
4 |
5 | Add `google/cloud-tools` to the `require-dev` section of your
6 | `composer.json`.
7 |
8 | You can also run the following command:
9 |
10 | ```
11 | $ composer require google/cloud-tools --dev
12 | ```
13 |
14 | ## Utilities
15 |
16 | ### flex_exec
17 |
18 | The cli script `src/Utils/Flex/flex_exec` is a tool for running a
19 | command with using the same Docker image as the application running on
20 | App Engine Flex.
21 |
22 | It spins up a Docker image of a Deployed App Engine Flexible App, and
23 | runs a command in that image. For example, if you are running Laravel
24 | application, you can invoke a command like `php artisan migrate` in
25 | the image.
26 |
27 | If the Flex application is requesting the cloudsql access
28 | (`beta_settings`, `cloud_sql_instances`), this tool also provides the
29 | connection to the same Cloud SQL instances.
30 |
31 | The command runs on virtual machines provided by [Google Cloud
32 | Container Builder](https://cloud.google.com/container-builder/docs/),
33 | and has access to the credentials of the Cloud Container Builder
34 | service account.
35 |
36 | ### Prerequisites
37 |
38 | To use flex_exec, you will need:
39 |
40 | * An app deployed to Google App Engine Flex
41 | * The gcloud SDK installed and configured. See https://cloud.google.com/sdk/
42 | * The `google/cloud-tools` composer package
43 |
44 | You may also need to grant the Cloud Container Builder service account
45 | any permissions needed by your command. For accessing Cloud SQL, you
46 | need to add `Cloud SQL Client` permission to the service account.
47 |
48 | You can find the service account configuration in the IAM tab in the
49 | Cloud Console under the name `[your-project-number]@cloudbuild.gserviceaccount.com`.
50 |
51 | ### Resource usage and billing
52 |
53 | The tool uses virtual machine resources provided by Google Cloud
54 | Container Builder. Although a certain number of usage minutes per day
55 | is covered under a free tier, additional compute usage beyond that
56 | time is billed to your Google Cloud account. For more details, see:
57 | https://cloud.google.com/container-builder/pricing
58 |
59 | If your command makes API calls or utilizes other cloud resources, you
60 | may also be billed for that usage. However, `flex_exec` does not use
61 | actual App Engine instances, and you will not be billed for additional
62 | App Engine instance usage.
63 |
64 | ### Example
65 |
66 | ```
67 | src/Utils/Flex/flex_exec run -- php artisan migrate
68 | ```
69 |
70 | ## Testing Utilities
71 |
72 | There are various test utilities in the `Google\Cloud\TestUtils` namespace.
73 |
74 | ## Test examples
75 |
76 | The example test cases are available in
77 | [`test/fixtures/appengine-standard`](https://github.com/GoogleCloudPlatform/php-tools/tree/main/test/fixtures/appengine-standard) directory.
78 |
79 | ### Environment variables
80 |
81 | There are multiple environment variables to control the behavior of
82 | our test traits.
83 |
84 | #### All Traits
85 |
86 | - `GOOGLE_PROJECT_ID`:
87 | The project id for deploying the application.
88 | - `GOOGLE_VERSION_ID`:
89 | The version id for deploying the application.
90 |
91 | #### AppEngineDeploymentTrait
92 |
93 | - `GOOGLE_DEPLOYMENT_DELAY`:
94 | Number of seconds to wait after the deployment has been triggered before continuing to execute tests.
95 | - `GOOGLE_KEEP_DEPLOYMENT`:
96 | Set to `true` to keep deployed app in place after test completion.
97 | - `GOOGLE_SKIP_DEPLOYMENT`:
98 | Set to `true` if you want to skip deployment.
99 | - `RUN_DEPLOYMENT_TESTS`:
100 | Set to `true` if you want to run deploy tests.
101 |
102 | #### DevAppserverTestTrait
103 |
104 | - `LOCAL_TEST_TARGETS`:
105 | You can specify multiple yaml files if your test need multiple services.
106 | - `PHP_CGI_PATH`:
107 | Path to `php-cgi` for running dev_appserver.
108 | - `RUN_DEVSERVER_TESTS`:
109 | Set to `true` if you want to run tests.
--------------------------------------------------------------------------------
/test/Utils/WordPress/wpGaeTest.php:
--------------------------------------------------------------------------------
1 | runCommand('create', [
40 | '--dir' => $dir,
41 | '--project_id' => $projectId,
42 | '--db_name' => $dbName,
43 | '--db_user' => $dbUser,
44 | '--db_password' => $dbPassword,
45 | '--db_instance' => $dbInstance,
46 | ]);
47 |
48 | $this->assertTrue(is_dir($dir));
49 | $files = ['app.yaml', 'wp-config.php'];
50 | foreach ($files as $file) {
51 | $this->assertFileExists($dir . '/' . $file);
52 | }
53 | // check the syntax of the rendered PHP file
54 | exec(sprintf('php -l %s/wp-config.php', $dir), $output, $ret);
55 | $this->assertEquals(0, $ret, implode(' ', $output));
56 |
57 | // check naively that variables were added
58 | $wpConfig = file_get_contents($dir . '/wp-config.php');
59 | $this->assertStringContainsString($projectId, $wpConfig);
60 | $this->assertStringContainsString($dbPassword, $wpConfig);
61 | }
62 |
63 | public function testUpdate()
64 | {
65 | $dir = sprintf('%s/wp-update-%s', sys_get_temp_dir(), time());
66 | mkdir($dir);
67 | $this->assertTrue(is_dir($dir));
68 |
69 | // these variables aren't actually taken into account, as we are just
70 | // testing the files get generated appropriately.
71 | $projectId = 'test-updated-project-id';
72 | $dbName = 'wordpress-db';
73 | $dbUser = 'wordpress-user';
74 | $dbPassword = 'test-db-password';
75 | $dbInstance = 'test-db-instance';
76 | $this->runCommand('update', [
77 | 'dir' => $dir,
78 | '--project_id' => $projectId,
79 | '--db_name' => $dbName,
80 | '--db_user' => $dbUser,
81 | '--db_password' => $dbPassword,
82 | '--db_instance' => $dbInstance,
83 | ]);
84 |
85 | $files = ['app.yaml', 'wp-config.php'];
86 | foreach ($files as $file) {
87 | $this->assertFileExists($dir . '/' . $file);
88 | }
89 |
90 | // check the syntax of the rendered PHP file
91 | exec(sprintf('php -l %s/wp-config.php', $dir), $output, $ret);
92 | $this->assertEquals(0, $ret, implode(' ', $output));
93 |
94 | // check naively that variables were added
95 | $wpConfig = file_get_contents($dir . '/wp-config.php');
96 | $this->assertStringContainsString($projectId, $wpConfig);
97 | $this->assertStringContainsString($dbPassword, $wpConfig);
98 | }
99 | }
100 |
--------------------------------------------------------------------------------
/test/TestUtils/EventuallyConsistentTestTraitTest.php:
--------------------------------------------------------------------------------
1 | catchAllExceptions = false;
38 | }
39 |
40 | public function testRunEventuallyConsistentTest()
41 | {
42 | $retries = 4;
43 | $i = 0;
44 | $func = function () use (&$i) {
45 | $i++;
46 | $this->assertTrue(false);
47 | };
48 | try {
49 | $this->runEventuallyConsistentTest($func, $retries);
50 | } catch (\Exception $e) {
51 | }
52 | $this->assertEquals($retries, $i);
53 | }
54 |
55 | public function testEventuallyConsistentTestReturnsValue()
56 | {
57 | $func = function () {
58 | return 'foo';
59 | };
60 | $retVal = $this->runEventuallyConsistentTest($func);
61 | $this->assertEquals('foo', $retVal);
62 | }
63 |
64 | public function testRetryCountInstanceVarTest()
65 | {
66 | $retries = 10;
67 | $i = 0;
68 | $func = function () use (&$i) {
69 | $i++;
70 | $this->assertTrue(false);
71 | };
72 | $this->eventuallyConsistentRetryCount = $retries;
73 | try {
74 | $this->runEventuallyConsistentTest($func);
75 | } catch (\Exception $e) {
76 | }
77 | $this->assertEquals($retries, $i);
78 | }
79 |
80 | public function testCatchAllExceptionsTest()
81 | {
82 | $retries = 4;
83 | $i = 0;
84 | $func = function () use (&$i) {
85 | $i++;
86 | throw new \Exception('Something goes wrong');
87 | };
88 | try {
89 | $this->runEventuallyConsistentTest($func, $retries, true);
90 | } catch (\Exception $e) {
91 | }
92 | $this->assertEquals($retries, $i);
93 | }
94 |
95 | public function testCatchAllExceptionsWithInstanceVarTest()
96 | {
97 | $retries = 4;
98 | $i = 0;
99 | $func = function () use (&$i) {
100 | $i++;
101 | throw new \Exception('Something goes wrong');
102 | };
103 | $this->catchAllExceptions = true;
104 | try {
105 | $this->runEventuallyConsistentTest($func, $retries);
106 | } catch (\Exception $e) {
107 | }
108 | $this->assertEquals($i, $retries);
109 | }
110 |
111 | public function testNoCatchAllExceptionsTest()
112 | {
113 | $retries = 4;
114 | $i = 0;
115 | $func = function () use (&$i) {
116 | $i++;
117 | throw new \Exception('Something goes wrong');
118 | };
119 | try {
120 | $this->runEventuallyConsistentTest($func, $retries);
121 | } catch (\Exception $e) {
122 | }
123 | $this->assertEquals(1, $i);
124 | }
125 | }
126 |
--------------------------------------------------------------------------------
/test/Utils/GcloudTest.php:
--------------------------------------------------------------------------------
1 | assertInstanceOf(\RuntimeException::class, $e);
62 | $this->assertEquals('gcloud failed', $e->getMessage());
63 | }
64 |
65 | public function testGcloudInitNotAuthenticated()
66 | {
67 | \Google\Cloud\Utils\mockExecInit(
68 | [],
69 | ['', 'my-project'],
70 | [0, 0],
71 | ['', 'my-project']
72 | );
73 | try {
74 | $gcloud = new Gcloud();
75 | } catch (\RuntimeException $e) {
76 | // Just pass, but $e is function level variable and
77 | // we can assert later outside of this block.
78 | }
79 | $this->assertInstanceOf(\RuntimeException::class, $e);
80 | $this->assertEquals('gcloud not authenticated', $e->getMessage());
81 | }
82 |
83 | public function testGcloudInitProjectNotSet()
84 | {
85 | \Google\Cloud\Utils\mockExecInit(
86 | [],
87 | ['user@example.com', ''],
88 | [0, 0],
89 | ['user@example.com', '']
90 | );
91 | try {
92 | $gcloud = new Gcloud();
93 | } catch (\RuntimeException $e) {
94 | // Just pass, but $e is function level variable and
95 | // we can assert later outside of this block.
96 | }
97 | $this->assertInstanceOf(\RuntimeException::class, $e);
98 | $this->assertEquals(
99 | 'gcloud project configuration not set',
100 | $e->getMessage()
101 | );
102 | }
103 |
104 | public function testGcloudExec()
105 | {
106 | \Google\Cloud\Utils\mockExecInit(
107 | [],
108 | ['user@example.com', 'my-project', 'output'],
109 | [0, 0, 0],
110 | ['user@example.com', 'my-project', 'result']
111 | );
112 | $gcloud = new Gcloud();
113 | list($ret, $output) = $gcloud->exec(['app', 'deploy', 'my dir']);
114 | $this->assertEquals(0, $ret);
115 | $this->assertEquals(['output'], $output);
116 | global $_commands;
117 | $this->assertEquals(
118 | "gcloud 'app' 'deploy' 'my dir'", array_pop($_commands)
119 | );
120 | }
121 | }
122 |
--------------------------------------------------------------------------------
/src/TestUtils/GcloudWrapper/GcloudWrapperTrait.php:
--------------------------------------------------------------------------------
1 | project = $project;
54 | if (empty($dir)) {
55 | $dir = getcwd();
56 | }
57 | $this->deployed = false;
58 | $this->isRunning = false;
59 | $this->dir = $dir;
60 | }
61 |
62 | private function errorLog($message)
63 | {
64 | fwrite(STDERR, $message . "\n");
65 | }
66 |
67 | protected function execWithRetry($cmd, $retries = 3, &$output = null)
68 | {
69 | for ($i = 0; $i <= $retries; ++$i) {
70 | exec($cmd, $output, $ret);
71 | if ($ret === 0) {
72 | return true;
73 | } elseif ($i <= $retries) {
74 | $this->errorLog('Retrying the command: ' . $cmd);
75 | }
76 | }
77 |
78 | return false;
79 | }
80 |
81 | /**
82 | * Retry the provided process and return the output.
83 | *
84 | * @param Symfony\Component\Process\Process $cmd
85 | * @param int $retries is the number of retry attempts to make.
86 | *
87 | * @return string
88 | * @throws \Symfony\Component\Process\Exception\ProcessFailedException
89 | */
90 | protected function runWithRetry(Process $cmd, $retries = 3)
91 | {
92 | $this->errorLog('Running: ' . str_replace("'", '', $cmd->getCommandLine()));
93 | for ($i = 0; $i <= $retries; ++$i) {
94 | // TODO: Use ExponentialBackoffTrait for more sophisticated handling.
95 | // Simple geometric backoff, .25 seconds * iteration.
96 | usleep(250000 * $i);
97 |
98 | $cmd->run();
99 | if ($cmd->isSuccessful()) {
100 | return $cmd->getOutput();
101 | } elseif ($i < $retries) {
102 | $this->errorLog('Retry Attempt #' . ($i + 1));
103 | $cmd->clearOutput();
104 | $cmd->clearErrorOutput();
105 | }
106 | }
107 |
108 | throw new ProcessFailedException($cmd);
109 | }
110 |
111 | /**
112 | * A setter for $dir, it's handy for using different directory for the
113 | * test.
114 | *
115 | * @param string $dir
116 | */
117 | public function setDir($dir)
118 | {
119 | $this->dir = $dir;
120 | }
121 |
122 | /**
123 | * Create \Symfony\Component\Process\Process with a given string.
124 | *
125 | * @param string $cmd
126 | *
127 | * @return Process
128 | */
129 | protected function createProcess($cmd, $dir = null, array $env = [])
130 | {
131 | return new Process(explode(' ', $cmd), $dir, $env);
132 | }
133 |
134 | /**
135 | * Stop the process.
136 | */
137 | public function stop()
138 | {
139 | if ($this->process->isRunning()) {
140 | $this->process->stop();
141 | }
142 | $this->isRunning = false;
143 | }
144 | }
145 |
--------------------------------------------------------------------------------
/src/TestUtils/CloudFunctionLocalTestTrait.php:
--------------------------------------------------------------------------------
1 | self::requireEnv('GOOGLE_PROJECT_ID')
51 | ];
52 | if (isset(self::$entryPoint)) {
53 | $props['entryPoint'] = self::$entryPoint;
54 | }
55 | if (isset(self::$functionSignatureType)) {
56 | $props['functionSignatureType'] = self::$functionSignatureType;
57 | }
58 | self::$fn = CloudFunction::fromArray(
59 | self::initFunctionProperties($props)
60 | );
61 | self::$localhost = self::doRun();
62 | }
63 |
64 | /**
65 | * Start the development server based on the prepared function.
66 | *
67 | * Allows configuring server properties, for example:
68 | *
69 | * return self::$fn->run(['FOO' => 'bar'], '9090', '/usr/local/bin/php');
70 | */
71 | private static function doRun()
72 | {
73 | return self::$fn->run();
74 | }
75 |
76 | /**
77 | * Customize startFunction properties.
78 | *
79 | * Example:
80 | *
81 | * $props['dir'] = 'path/to/function-dir';
82 | * $props['region'] = 'us-west1';
83 | * return $props;
84 | */
85 | private static function initFunctionProperties(array $props = [])
86 | {
87 | return $props;
88 | }
89 |
90 | /**
91 | * Set up the client.
92 | *
93 | * @before
94 | */
95 | public function setUpClient()
96 | {
97 | $baseUrl = self::$fn->getLocalBaseUrl();
98 | $this->client = new Client([
99 | 'base_uri' => $baseUrl,
100 | 'http_errors' => false
101 | ]);
102 | }
103 |
104 | /**
105 | * Stop the function.
106 | *
107 | * @afterClass
108 | */
109 | public static function stopFunction()
110 | {
111 | self::$fn->stop();
112 | }
113 |
114 | /**
115 | * Make a request using a cloud event
116 | *
117 | * Example:
118 | *
119 | * $this->request(CloudEvent::fromArray($params));
120 | */
121 | private function request(CloudEvent $cloudevent, array $params = [])
122 | {
123 | $cloudEventHeaders = array_filter([
124 | 'ce-id' => $cloudevent->getId(),
125 | 'ce-source' => $cloudevent->getSource(),
126 | 'ce-specversion' => $cloudevent->getSpecVersion(),
127 | 'ce-type' => $cloudevent->getType(),
128 | 'ce-datacontenttype' => $cloudevent->getDataContentType(),
129 | 'ce-dataschema' => $cloudevent->getDataSchema(),
130 | 'ce-subject' => $cloudevent->getSubject(),
131 | 'ce-time' => $cloudevent->getTime(),
132 | ]);
133 |
134 | return $this->client->request('POST', '/', array_merge_recursive(
135 | $params,
136 | [
137 | 'json' => $cloudevent->getData(),
138 | 'headers' => $cloudEventHeaders,
139 | ]
140 | ));
141 | }
142 | }
143 |
--------------------------------------------------------------------------------
/src/Utils/ContainerExec.php:
--------------------------------------------------------------------------------
1 | gcloud = ($gcloud == null) ? new Gcloud() : $gcloud;
65 | if (class_exists(\Twig_Loader_Filesystem::class)
66 | && class_exists(\Twig_Environment::class)) {
67 | $loader = new \Twig_Loader_Filesystem(__DIR__ . '/templates');
68 | $this->twig = new \Twig_Environment($loader);
69 | } else {
70 | $loader = new \Twig\Loader\FilesystemLoader(__DIR__ . '/templates');
71 | $this->twig = new \Twig\Environment($loader);
72 | }
73 | $this->image = $image;
74 | $this->commands = $commands;
75 | $this->cloudSqlInstances = $cloudSqlInstances;
76 | $this->workdir = $workdir;
77 | }
78 |
79 | /**
80 | * Run the commands within the image, using Cloud Container Builder
81 | *
82 | * @return string The output of the relevant build step of the Container
83 | * Builder job.
84 | * @throws \RuntimeException thrown when the command failed
85 | */
86 | public function run()
87 | {
88 | $template = $this->twig->load('cloudbuild.yaml.tmpl');
89 | $context = [
90 | 'cloud_sql_instances' => $this->cloudSqlInstances,
91 | 'cloud_sql_proxy_image' => self::CLOUD_SQL_PROXY_IMAGE,
92 | 'target_image' => $this->image,
93 | 'commands' => implode(
94 | ',',
95 | array_map('escapeshellarg', $this->commands)
96 | )
97 | ];
98 | $cloudBuildYaml = $template->render($context);
99 | file_put_contents("$this->workdir/cloudbuild.yaml", $cloudBuildYaml);
100 | list($result, $cmdOutput) = $this->gcloud->exec(
101 | [
102 | 'builds',
103 | 'submit',
104 | "--config=$this->workdir/cloudbuild.yaml",
105 | "$this->workdir"
106 | ]
107 | );
108 | file_put_contents(
109 | "$this->workdir/cloudbuild.log",
110 | implode(PHP_EOL, $cmdOutput)
111 | );
112 | if ($result !== 0) {
113 | throw new \RuntimeException('Failed to run the command');
114 | }
115 | $ret = '';
116 | if ($this->cloudSqlInstances) {
117 | $targetStep = 'Step #3';
118 | } else {
119 | $targetStep = 'Step #1';
120 | }
121 | foreach ($cmdOutput as $line) {
122 | if (\strpos($line, $targetStep) !== false) {
123 | $ret .= $line . PHP_EOL;
124 | }
125 | }
126 | return $ret;
127 | }
128 | }
129 |
--------------------------------------------------------------------------------
/.github/workflows/doctum.yml:
--------------------------------------------------------------------------------
1 | name: Generate Reference Documentation
2 |
3 | on:
4 | workflow_call:
5 | inputs:
6 | title:
7 | type: string
8 | default: "Reference Documentation"
9 | required: true
10 | theme:
11 | type: string
12 | description: "doctum theme"
13 | default: default
14 | default_version:
15 | type: string
16 | description: "The version tag to use as the latest version."
17 | tag_pattern:
18 | type: string
19 | description: "tags to include in version selector"
20 | default: "v1.*"
21 | dry_run:
22 | type: boolean
23 | description: "do not deploy to gh-pages"
24 | exclude_file:
25 | type: string
26 | description: "exclude a file from documentation"
27 |
28 | jobs:
29 | docs:
30 | name: "Generate and Deploy Documentation"
31 | runs-on: ubuntu-latest
32 | steps:
33 | - name: Checkout
34 | uses: actions/checkout@v4
35 | with:
36 | fetch-depth: 0
37 | - name: Setup PHP
38 | uses: shivammathur/setup-php@v2
39 | with:
40 | php-version: 8.1
41 | - name: Download Doctum
42 | run: |
43 | curl -# https://doctum.long-term.support/releases/5.5/doctum.phar -o doctum.phar
44 | curl -# https://doctum.long-term.support/releases/5.5/doctum.phar.sha256 -o doctum.phar.sha256
45 | sha256sum --strict --check doctum.phar.sha256
46 | - name: Generate Doctum Config
47 | run: |
48 | DOCTUM_CONFIG=$(cat <<'EOF'
49 | files()
60 | ->name('*.php')
61 | ->notName('${{ inputs.exclude_file }}')
62 | ->exclude('GPBMetadata')
63 | ->in(__DIR__ . '/src');
64 |
65 | $versions = GitVersionCollection::create(__DIR__);
66 | if ($tagPattern) {
67 | $versions->addFromTags($tagPattern);
68 | }
69 | if ($defaultVersion) {
70 | $versions->add($defaultVersion, $defaultVersion);
71 | }
72 |
73 | return new Doctum($iterator, [
74 | 'title' => '${{ inputs.title }}',
75 | 'theme' => '${{ inputs.theme }}',
76 | 'versions' => $versions,
77 | 'build_dir' => __DIR__ . '/.build/%version%',
78 | 'cache_dir' => __DIR__ . '/.cache/%version%',
79 | 'remote_repository' => new GitHubRemoteRepository('${{ github.repository }}', __DIR__),
80 | 'default_opened_level' => 2,
81 | 'template_dirs' => [__DIR__ . '/.github'],
82 | ]);
83 | EOF
84 | )
85 | echo "$DOCTUM_CONFIG"; # for debugging
86 | echo "$DOCTUM_CONFIG" > doctum-config.php;
87 | - name: Run Doctum to Generate Documentation
88 | run: |
89 | php doctum.phar update doctum-config.php --ignore-parse-errors
90 | if [ ! -d .build ]; then
91 | echo "Action did not generate any documentation. Did you forget to provide a default_version or tag_pattern?";
92 | exit 1;
93 | fi
94 | - if: inputs.default_version
95 | name: Redirect Index to Latest Version
96 | run: |
97 | cat << EOF > .build/index.html
98 |
99 | EOF
100 | - if: ${{ !inputs.dry_run }}
101 | name: Move generated files into GitHub Pages branch
102 | run: |
103 | git submodule add -q -f -b gh-pages https://github.com/${{ github.repository }} .ghpages
104 | rsync -aP .build/* .ghpages/
105 | - if: ${{ !inputs.dry_run }}
106 | name: Deploy 🚀
107 | uses: JamesIves/github-pages-deploy-action@releases/v3
108 | with:
109 | ACCESS_TOKEN: ${{ secrets.GITHUB_TOKEN }}
110 | BRANCH: gh-pages
111 | FOLDER: .ghpages
112 |
--------------------------------------------------------------------------------
/test/Utils/Flex/FlexExecCommandTest.php:
--------------------------------------------------------------------------------
1 | gcloud = $this->prophesize(Gcloud::class);
46 | $this->fs = new Filesystem();
47 | $this->tempnam = tempnam(sys_get_temp_dir(), 'flex-exec-test');
48 | $this->workdir = $this->tempnam . '_workdir';
49 | try {
50 | $this->fs->mkdir($this->workdir);
51 | } catch (IOExceptionInterface $e) {
52 | $this->fail("Failed to crete a workdir: " . $e->getTraceAsString());
53 | }
54 | }
55 |
56 | public function tearDown(): void
57 | {
58 | unlink($this->tempnam);
59 | $this->fs->remove($this->workdir);
60 | }
61 |
62 | /**
63 | * @dataProvider dataProvider
64 | */
65 | public function testFlexExecCommand($describeResult, $cloudbuildYaml)
66 | {
67 | $version = 'my-version';
68 | $this->gcloud->exec(
69 | [
70 | 'app',
71 | 'versions',
72 | 'list',
73 | '--service=default',
74 | "--format=get(version.id)",
75 | "--sort-by=~version.createTime",
76 | "--limit=1"
77 | ]
78 | )
79 | ->shouldBeCalledTimes(1)
80 | ->willReturn(
81 | [
82 | 0,
83 | [$version]
84 | ]
85 | );
86 | $this->gcloud->exec(
87 | [
88 | 'app',
89 | 'versions',
90 | 'describe',
91 | $version,
92 | "--service=default",
93 | "--format=json"
94 | ]
95 | )
96 | ->shouldBeCalledTimes(1)
97 | ->willReturn(
98 | [
99 | 0,
100 | explode(
101 | PHP_EOL,
102 | file_get_contents($describeResult)
103 | )
104 | ]
105 | );
106 | $this->gcloud->exec(
107 | [
108 | 'builds',
109 | 'submit',
110 | "--config=$this->workdir/cloudbuild.yaml",
111 | "$this->workdir"
112 | ]
113 | )
114 | ->shouldBeCalledTimes(1)
115 | ->willReturn(
116 | [
117 | 0,
118 | ['Build succeeded']
119 | ]
120 | );
121 | $flexExecCommand = new FlexExecCommand($this->gcloud->reveal());
122 | $commandTester = new CommandTester($flexExecCommand);
123 | $commandTester->execute(
124 | [
125 | 'commands' => ['ls', 'my dir'],
126 | '--preserve-workdir' => true,
127 | '--workdir' => $this->workdir
128 | ]
129 | );
130 | // Check the contents of the generated cloudbuild.yaml
131 | $this->assertFileEquals(
132 | $cloudbuildYaml,
133 | sprintf('%s/cloudbuild.yaml', $this->workdir)
134 | );
135 | }
136 |
137 | public function dataProvider()
138 | {
139 | return [
140 | [
141 | // with cloud-sql-proxy
142 | __DIR__ . '/data/cloudsql-describe-result',
143 | __DIR__ . '/data/cloudsql-cloudbuild-yaml'
144 | ],
145 | [
146 | // without cloud-sql-proxy
147 | __DIR__ . '/data/basic-describe-result',
148 | __DIR__ . '/data/basic-cloudbuild-yaml'
149 | ],
150 | ];
151 | }
152 | }
153 |
--------------------------------------------------------------------------------
/src/TestUtils/ExecuteCommandTrait.php:
--------------------------------------------------------------------------------
1 | get($commandName);
37 | $commandTester = new CommandTester($command);
38 |
39 | $testFunc = function () use ($commandTester, $args) {
40 | ob_start();
41 | try {
42 | $commandTester->execute($args, ['interactive' => false]);
43 | } finally {
44 | // Ensure output buffer is clean even if an exception is thrown.
45 | $output = ob_get_clean();
46 | }
47 | return $output;
48 | };
49 |
50 | if (self::$backoff) {
51 | return self::$backoff->execute($testFunc);
52 | }
53 |
54 | return $testFunc();
55 | }
56 |
57 | public static function setWorkingDirectory($workingDirectory)
58 | {
59 | self::$workingDirectory = $workingDirectory;
60 | }
61 |
62 | /**
63 | * run a command
64 | *
65 | * @param $cmd
66 | * @throws \Exception
67 | */
68 | public static function execute($cmd, $timeout = false)
69 | {
70 | $process = self::createProcess($cmd, $timeout);
71 | self::executeProcess($process);
72 |
73 | return $process->getOutput();
74 | }
75 |
76 | /**
77 | * Executes a Process and throws an exception
78 | *
79 | * @param Process $process
80 | * @param bool $throwExceptionOnFailure
81 | * @throws \Exception
82 | */
83 | private static function executeProcess(Process $process, $throwExceptionOnFailure = true)
84 | {
85 | if (self::$logger) {
86 | self::$logger->debug(sprintf('Executing: %s', $process->getCommandLine()));
87 | }
88 |
89 | $process->run(self::getCallback());
90 |
91 | if (!$process->isSuccessful() && $throwExceptionOnFailure) {
92 | $output = $process->getErrorOutput() ? $process->getErrorOutput() : $process->getOutput();
93 | $msg = sprintf('Error executing "%s": %s', $process->getCommandLine(), $output);
94 |
95 | throw new \Exception($msg);
96 | }
97 |
98 | return $process->isSuccessful();
99 | }
100 |
101 | /**
102 | * @return Process
103 | */
104 | private static function createProcess($cmd, $timeout = false)
105 | {
106 | $process = is_array($cmd) ?
107 | new Process($cmd) :
108 | Process::fromShellCommandline($cmd);
109 |
110 | if (self::$workingDirectory) {
111 | $process->setWorkingDirectory(self::$workingDirectory);
112 | }
113 |
114 | if (false !== $timeout) {
115 | $process->setTimeout($timeout);
116 | }
117 |
118 | return $process;
119 | }
120 |
121 | private static function getCallback()
122 | {
123 | if (self::$logger) {
124 | $logger = self::$logger;
125 | return function ($type, $line) use ($logger) {
126 | if ($type === 'err') {
127 | $logger->error($line);
128 | } else {
129 | $logger->debug($line);
130 | }
131 | };
132 | }
133 | }
134 |
135 | protected static function executeWithRetry($cmd, $timeout = false, $retries = 3)
136 | {
137 | $process = self::createProcess($cmd, $timeout);
138 | for ($i = 0; $i <= $retries; $i++) {
139 | // only allow throwing exceptions on final attempt
140 | $throwExceptionOnFailure = $i == $retries;
141 | if (self::executeProcess($process, $throwExceptionOnFailure)) {
142 | return true;
143 | }
144 | if (self::$logger && $i < $retries) {
145 | self::$logger->debug('Retrying the command: ' . $cmd);
146 | }
147 | }
148 | return false;
149 | }
150 | }
151 |
--------------------------------------------------------------------------------
/test/Utils/WordPress/ProjectTest.php:
--------------------------------------------------------------------------------
1 | createMock(InputInterface::class);
36 | $output = $this->createMock(OutputInterface::class);
37 | $helper = $this->createMock(QuestionHelper::class);
38 | $i = 0;
39 | $input
40 | ->expects($this->exactly(7))
41 | ->method('getOption')
42 | ->will($this->returnCallback(function ($optionName) {
43 | if ($optionName == 'dir') {
44 | return sys_get_temp_dir() . '/wp-project' . rand();
45 | }
46 | if ($optionName == 'db_region') {
47 | return 'us-central1';
48 | }
49 | return null;
50 | }));
51 | $input
52 | ->expects($this->any())
53 | ->method('isInteractive')
54 | ->will($this->returnValue(true));
55 | $helper
56 | ->expects($this->any())
57 | ->method('ask')
58 | ->will($this->returnCallback(function ($optionName) use (&$i) {
59 | return 'value_' . $i++;
60 | }));
61 |
62 | $project = new Project($input, $output, $helper);
63 | $project->initializeProject();
64 | $params = $project->initializeDatabase();
65 |
66 | $this->assertEquals([
67 | 'project_id' => 'value_1',
68 | 'db_instance' => 'value_2',
69 | 'db_name' => 'value_3',
70 | 'db_user' => 'value_4',
71 | 'db_password' => 'value_5',
72 | 'db_connection' => 'value_1:us-central1:value_2',
73 | 'local_db_user' => 'value_4',
74 | 'local_db_password' => 'value_5',
75 | ], $params);
76 | }
77 |
78 | public function testDownloadWordPress()
79 | {
80 | $input = $this->createMock(InputInterface::class);
81 | $output = $this->createMock(OutputInterface::class);
82 | $input
83 | ->expects($this->exactly(2))
84 | ->method('getOption')
85 | ->with($this->logicalOr(
86 | $this->equalTo('dir'),
87 | $this->equalTo('wordpress_url')
88 | ))
89 | ->will($this->returnCallback(function ($optionName) {
90 | if ($optionName == 'dir') {
91 | return sys_get_temp_dir() . '/wp-project' . rand();
92 | }
93 | return Project::LATEST_WP;
94 | }));
95 |
96 | $project = new Project($input, $output);
97 | $dir = $project->initializeProject();
98 | $project->downloadWordpress();
99 | $this->assertFileExists($dir . '/wordpress/wp-login.php');
100 |
101 | // test downloading a plugin
102 | $project->downloadGcsPlugin();
103 | $this->assertFileExists($dir . '/wordpress/wp-content/plugins/gcs/readme.txt');
104 | }
105 |
106 | public function testDownloadWordPressToDifferentDirectory()
107 | {
108 | $input = $this->createMock(InputInterface::class);
109 | $output = $this->createMock(OutputInterface::class);
110 | $dir = sys_get_temp_dir() . '/wp-project' . rand();
111 | $input
112 | ->expects($this->once())
113 | ->method('getOption')
114 | ->with($this->logicalOr(
115 | $this->equalTo('dir'),
116 | $this->equalTo('wordpress_url')
117 | ))
118 | ->will($this->returnCallback(function ($optionName) {
119 | return Project::LATEST_WP;
120 | }));
121 |
122 | $project = new Project($input, $output);
123 | $project->downloadWordpress($dir);
124 | $project->initializeProject($dir);
125 |
126 | $this->assertFileExists($dir . '/wp-login.php');
127 |
128 | // test downloading a plugin
129 | $project->downloadGcsPlugin();
130 | $this->assertFileExists($dir . '/wp-content/plugins/gcs/readme.txt');
131 | }
132 | }
133 |
--------------------------------------------------------------------------------
/src/Utils/WordPress/files/wp-config.php:
--------------------------------------------------------------------------------
1 | retries = $retries !== null ? (int) $retries : 3;
65 | $this->retryFunction = $retryFunction;
66 | // @todo revisit this approach
67 | // @codeCoverageIgnoreStart
68 | $this->delayFunction = function ($delay) {
69 | usleep($delay);
70 | };
71 | // @codeCoverageIgnoreEnd
72 | }
73 |
74 | /**
75 | * Executes the retry process.
76 | *
77 | * @param callable $function
78 | * @param array $arguments [optional]
79 | * @return mixed
80 | * @throws \Exception The last exception caught while retrying.
81 | */
82 | public function execute(callable $function, array $arguments = [])
83 | {
84 | $delayFunction = $this->delayFunction;
85 | $calcDelayFunction = $this->calcDelayFunction ?: [$this, 'calculateDelay'];
86 | $exception = null;
87 |
88 | while (true) {
89 | try {
90 | return call_user_func_array($function, $arguments);
91 | } catch (\Exception $exception) {
92 | if (!$this->shouldRetry($exception)) {
93 | throw $exception;
94 | }
95 |
96 | $delayFunction($calcDelayFunction($this->retryAttempt));
97 | }
98 | }
99 |
100 | throw $exception;
101 | }
102 |
103 | /**
104 | * Configure this backoff to call another backoff retry function if this
105 | * backoff's retry function returns false.
106 | *
107 | * @param ExponentialBackoff $backoff
108 | * @return null
109 | */
110 | public function combine(ExponentialBackoff $backoff)
111 | {
112 | $this->backoff = $backoff;
113 | }
114 |
115 | /**
116 | * Function which returns bool for whether or not to retry.
117 | *
118 | * @param Exception $exception
119 | * @return bool
120 | */
121 | protected function shouldRetry(\Exception $exception)
122 | {
123 | if ($this->retryAttempt < $this->retries) {
124 | if (!$this->retryFunction) {
125 | $this->retryAttempt++;
126 | return true;
127 | }
128 | if (call_user_func($this->retryFunction, $exception)) {
129 | $this->retryAttempt++;
130 | return true;
131 | }
132 | }
133 |
134 | if ($this->backoff && $this->backoff->shouldRetry($exception)) {
135 | return true;
136 | }
137 |
138 | return false;
139 | }
140 |
141 | /**
142 | * If not set, defaults to using `usleep`.
143 | *
144 | * @param callable $delayFunction
145 | * @return void
146 | */
147 | public function setDelayFunction(callable $delayFunction)
148 | {
149 | $this->delayFunction = $delayFunction;
150 | }
151 |
152 | /**
153 | * If not set, defaults to using
154 | * {@see Google\Cloud\Core\ExponentialBackoff::calculateDelay()}.
155 | *
156 | * @param callable $calcDelayFunction
157 | * @return void
158 | */
159 | public function setCalcDelayFunction(callable $calcDelayFunction)
160 | {
161 | $this->calcDelayFunction = $calcDelayFunction;
162 | }
163 |
164 | /**
165 | * Calculates exponential delay.
166 | *
167 | * @param int $attempt The attempt number used to calculate the delay.
168 | * @return int
169 | */
170 | public static function calculateDelay($attempt)
171 | {
172 | return min(
173 | mt_rand(0, 1000000) + (pow(2, $attempt) * 1000000),
174 | self::MAX_DELAY_MICROSECONDS
175 | );
176 | }
177 | }
178 |
--------------------------------------------------------------------------------
/src/Utils/WordPress/wp-gae:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env php
2 | add(new Command('create'))
49 | ->setDescription('Create a new WordPress site for Google Cloud')
50 | ->setDefinition(clone $wordPressOptions)
51 | ->addOption('dir', null, InputOption::VALUE_REQUIRED, 'Directory for the new project', Project::DEFAULT_DIR)
52 | ->addOption('wordpress_url', null, InputOption::VALUE_REQUIRED, 'URL of the WordPress archive', Project::LATEST_WP)
53 | ->setCode(function (InputInterface $input, OutputInterface $output) {
54 | $wordpress = new Project($input, $output);
55 | // Run the wizard to prompt user for project and database parameters.
56 | $dir = $wordpress->promptForProjectDir();
57 |
58 | // download wordpress
59 | $wordpress->downloadWordpress($dir);
60 |
61 | // initialize the project and download the plugins
62 | $wordpress->initializeProject($dir);
63 | $wordpress->downloadGcsPlugin();
64 |
65 | $dbParams = $wordpress->initializeDatabase();
66 |
67 | // populate random key params
68 | $params = $dbParams + $wordpress->generateRandomValueParams();
69 |
70 | // copy all the sample files into the project dir and replace parameters
71 | $wordpress->copyFiles(__DIR__ . '/files', [
72 | 'app.yaml' => '/',
73 | 'cron.yaml' => '/',
74 | 'php.ini' => '/',
75 | 'gae-app.php' => '/',
76 | 'wp-config.php' => '/',
77 | ], $params);
78 |
79 | $output->writeln("Your WordPress project is ready at $dir");
80 | });
81 |
82 | $application->add(new Command('update'))
83 | ->setDescription('Update an existing WordPress site for Google Cloud')
84 | ->setDefinition(clone $wordPressOptions)
85 | ->addArgument('dir', InputArgument::REQUIRED, 'Directory for the existing project')
86 | ->setCode(function (InputInterface $input, OutputInterface $output) {
87 | // use the supplied dir for the wordpress project
88 | $dir = $input->getArgument('dir');
89 |
90 | $wordpress = new Project($input, $output);
91 | $wordpress->initializeProject($dir);
92 |
93 | // Download the plugins if they don't exist
94 | if (!file_exists($dir . '/wp-content/plugins/gcs')) {
95 | $wordpress->downloadGcsPlugin();
96 | }
97 |
98 | $dbParams = $wordpress->initializeDatabase();
99 |
100 | // populate random key params
101 | $params = $dbParams + $wordpress->generateRandomValueParams();
102 |
103 | // copy all the sample files into the project dir and replace parameters
104 | $wordpress->copyFiles(__DIR__ . '/files', [
105 | 'app.yaml' => '/',
106 | 'cron.yaml' => '/',
107 | 'php.ini' => '/',
108 | 'gae-app.php' => '/',
109 | 'wp-config.php' => '/',
110 | ], $params);
111 |
112 | $output->writeln("Your WordPress project has been updated at $dir");
113 | });
114 |
115 | if (getenv('PHPUNIT_TESTS') === '1') {
116 | return $application;
117 | }
118 |
119 | $application->run();
120 |
--------------------------------------------------------------------------------
/src/Utils/Project.php:
--------------------------------------------------------------------------------
1 | dir = $this->validateProjectDir($dir);
54 | }
55 |
56 | protected function validateProjectDir($dir)
57 | {
58 | if ($this->isRelativePath($dir)) {
59 | $dir = getcwd() . DIRECTORY_SEPARATOR . $dir;
60 | }
61 | if (is_file($dir)) {
62 | $this->errors[] = 'File exists: ' . $dir;
63 | return;
64 | }
65 | if (is_dir($dir)) {
66 | $this->info[] = 'Re-using a directory ' . $dir . '.';
67 | } elseif (!@mkdir($dir, 0750, true)) {
68 | $this->errors[] = 'Can not create a directory: ' . $dir;
69 | } else {
70 | $this->info[] = 'A directory ' . $dir . ' was created.';
71 | }
72 | return realpath($dir);
73 | }
74 |
75 | public function downloadArchive($name, $url, $dir = '')
76 | {
77 | $tmpdir = sys_get_temp_dir();
78 | $file = $tmpdir . DIRECTORY_SEPARATOR . basename($url);
79 | file_put_contents($file, file_get_contents($url));
80 | $dir = $this->getRelativeDir($dir);
81 |
82 | if (substr($url, -3, 3) === 'zip') {
83 | $zip = new \ZipArchive();
84 | if ($zip->open($file) === false) {
85 | $this->errors[] = 'Failed to open a zip file: ' . $file;
86 | return;
87 | }
88 | if ($zip->extractTo($dir) === false) {
89 | $this->errors[] = 'Failed to extract a zip file: ' . $file;
90 | $zip->close();
91 | return;
92 | }
93 | $zip->close();
94 | } else {
95 | $phar = new \PharData($file, 0, null);
96 | $phar->extractTo($dir, null, true);
97 | }
98 | unlink($file);
99 | $this->info[] = 'Downloaded ' . $name . '.';
100 | }
101 |
102 | public function copyFiles($path, $files, $params = [])
103 | {
104 | foreach ($files as $file => $target) {
105 | $dest = $this->dir . $target . $file;
106 | touch($dest);
107 | chmod($dest, 0640);
108 | $content = file_get_contents($path . DIRECTORY_SEPARATOR . $file);
109 | if ($params) {
110 | $content = strtr($content, $params);
111 | }
112 | file_put_contents($dest, $content, LOCK_EX);
113 | }
114 | $this->info[] = 'Copied necessary files with parameters.';
115 | }
116 |
117 | public function runComposer()
118 | {
119 | chdir($this->dir);
120 | exec(
121 | 'composer update --no-interaction --no-progress --no-ansi',
122 | $output,
123 | $ret
124 | );
125 | $this->info = array_merge($this->info, $output);
126 | if ($ret !== 0) {
127 | $this->errors[] = 'Failed to run composer update in ' . $this->dir
128 | . '. Please run it by yourself before running WordPress.';
129 | }
130 | }
131 |
132 | public function getDir()
133 | {
134 | return $this->dir;
135 | }
136 |
137 | public function getInfo()
138 | {
139 | $ret = $this->info;
140 | $this->info = [];
141 |
142 | return $ret;
143 | }
144 |
145 | public function getErrors()
146 | {
147 | if (empty($this->errors)) {
148 | return false;
149 | }
150 | return $this->errors;
151 | }
152 |
153 | public static function getAvailableDbRegions()
154 | {
155 | return self::$availableDbRegions;
156 | }
157 |
158 | protected function getRelativeDir($dir)
159 | {
160 | return $this->isRelativePath($dir)
161 | ? $this->dir . DIRECTORY_SEPARATOR . $dir
162 | : $dir;
163 | }
164 |
165 | private function isRelativePath($path)
166 | {
167 | if (strlen($path) === 0) {
168 | return true;
169 | }
170 | if (strtoupper(substr(PHP_OS, 0, 3)) === 'WIN') {
171 | return !preg_match('/^[a-z]+\:\\\\/i', $path);
172 | }
173 | return strpos($path, DIRECTORY_SEPARATOR) !== 0;
174 | }
175 | }
176 |
--------------------------------------------------------------------------------