├── 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 |

Google Cloud Sample App

33 |
34 |

List Secrets

35 |
36 | listSecrets($parent) as $secret): ?> 37 |
38 | 39 |
40 | 41 |

List DLP Jobs

42 |
43 | listDlpJobs($parent) as $job): ?> 44 |
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 |

Google Cloud Sample App

40 |
41 |

List Secrets

42 |
43 | listSecrets($listSecretsRequest) as $secret): ?> 44 |
45 | 46 |
47 | 48 |

List DLP Jobs

49 |
50 | listDlpJobs($listDlpJobsRequest) as $job): ?> 51 |
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 | --------------------------------------------------------------------------------