├── .gitignore ├── .kokoro ├── common.cfg ├── php72.cfg ├── php73.cfg ├── system_tests.sh └── trampoline.sh ├── .php_cs.dist ├── CODEOWNERS ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── authenticate-users ├── .gitignore ├── README.md ├── app.yaml ├── composer.json ├── index.php ├── phpunit.xml.dist └── test │ ├── DeployTest.php │ └── indexTest.php ├── background-processing ├── README.md ├── app │ ├── .gitignore │ ├── app.yaml │ ├── composer.json │ ├── index.php │ ├── php.ini │ ├── resources │ │ └── views │ │ │ └── home.php │ └── routes │ │ └── web.php ├── backend │ ├── .dockerignore │ ├── .gitignore │ ├── Dockerfile │ ├── composer.json │ ├── index.php │ └── php.ini └── test │ ├── app │ └── AppTest.php │ ├── backend │ └── translateTest.php │ ├── phpunit-app.xml.dist │ ├── phpunit-backend.xml.dist │ ├── phpunit-system.xml.dist │ └── system │ └── DeployTest.php ├── bookshelf ├── .gitignore ├── README.md ├── app.yaml ├── app │ └── Exceptions │ │ └── Handler.php ├── composer.json ├── images │ └── moby-dick.jpg ├── index.php ├── php.ini ├── phpunit.xml.dist ├── resources │ └── views │ │ ├── base.php │ │ ├── form.php │ │ ├── list.php │ │ └── show.php ├── routes │ └── web.php └── test │ ├── ControllersTest.php │ └── DeployTest.php ├── gce ├── .gitignore ├── README.md ├── composer.json ├── config │ ├── fluentd │ │ └── helloworld.conf │ └── nginx │ │ ├── fastcgi_params │ │ └── helloworld.conf ├── index.php ├── phpunit.xml.dist ├── routes │ └── web.php ├── scripts │ ├── deploy.sh │ ├── startup-script.sh │ └── teardown.sh └── test │ ├── AppTest.php │ └── DeployTest.php ├── renovate.json └── sessions ├── .gitignore ├── README.md ├── app.yaml ├── composer.json ├── index.php ├── php.ini ├── phpunit.xml.dist └── test └── DeployTest.php /.gitignore: -------------------------------------------------------------------------------- 1 | .php_cs.cache 2 | composer.lock 3 | .gcloudignore 4 | -------------------------------------------------------------------------------- /.kokoro/common.cfg: -------------------------------------------------------------------------------- 1 | # Format: //devtools/kokoro/config/proto/build.proto 2 | 3 | # Download trampoline resources. These will be in ${KOKORO_GFILE_DIR} 4 | gfile_resources: "/bigstore/cloud-devrel-kokoro-resources/trampoline" 5 | 6 | # Download credentials from Cloud Storage. 7 | gfile_resources: "/bigstore/cloud-devrel-kokoro-resources/getting-started-php" 8 | 9 | # All builds use the trampoline script to run in docker. 10 | build_file: "getting-started-php/.kokoro/trampoline.sh" 11 | 12 | # Configure the docker image for kokoro-trampoline. 13 | env_vars: { 14 | key: "TRAMPOLINE_BUILD_FILE" 15 | value: "github/getting-started-php/.kokoro/system_tests.sh" 16 | } 17 | -------------------------------------------------------------------------------- /.kokoro/php72.cfg: -------------------------------------------------------------------------------- 1 | # Format: //devtools/kokoro/config/proto/build.proto 2 | 3 | # Configure the docker image for kokoro-trampoline. 4 | env_vars: { 5 | key: "TRAMPOLINE_IMAGE" 6 | value: "gcr.io/cloud-devrel-kokoro-resources/php72" 7 | } 8 | 9 | -------------------------------------------------------------------------------- /.kokoro/php73.cfg: -------------------------------------------------------------------------------- 1 | # Format: //devtools/kokoro/config/proto/build.proto 2 | 3 | # Configure the docker image for kokoro-trampoline. 4 | env_vars: { 5 | key: "TRAMPOLINE_IMAGE" 6 | value: "gcr.io/cloud-devrel-kokoro-resources/php73" 7 | } 8 | 9 | env_vars: { 10 | key: "RUN_CS_CHECK" 11 | value: "true" 12 | } 13 | 14 | env_vars: { 15 | key: "RUN_DEPLOYMENT_TESTS" 16 | value: "true" 17 | } 18 | -------------------------------------------------------------------------------- /.kokoro/system_tests.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Copyright 2020 Google LLC 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | 17 | set -e 18 | 19 | # Kokoro directory for running these samples 20 | if [ -d github/getting-started-php ]; then 21 | cd github/getting-started-php 22 | fi 23 | 24 | # Run code standards check when appropriate 25 | if [ "${RUN_CS_CHECK}" = "true" ]; then 26 | wget https://cs.symfony.com/download/php-cs-fixer-v2.phar -O php-cs-fixer 27 | chmod a+x php-cs-fixer 28 | PHP_CS_FIXER_IGNORE_ENV=1 ./php-cs-fixer fix --dry-run --diff 29 | fi 30 | 31 | if [ -f $KOKORO_GFILE_DIR/service-account.json ]; then 32 | export GOOGLE_APPLICATION_CREDENTIALS=$KOKORO_GFILE_DIR/service-account.json 33 | elif [ "$GOOGLE_APPLICATION_CREDENTIALS" = "" ]; then 34 | echo "GOOGLE_APPLICATION_CREDENTIALS not found" 35 | exit 1 36 | fi 37 | 38 | export GOOGLE_PROJECT_ID=$(cat "${GOOGLE_APPLICATION_CREDENTIALS}" | jq -r .project_id) 39 | export GOOGLE_CLOUD_PROJECT=$GOOGLE_PROJECT_ID 40 | export GOOGLE_VERSION_ID=$KOKORO_BUILD_NUMBER 41 | export PULL_REQUEST_NUMBER=$KOKORO_GITHUB_PULL_REQUEST_NUMBER 42 | 43 | # Activate the service account 44 | if [ -f ${GOOGLE_APPLICATION_CREDENTIALS} ]; then 45 | gcloud auth activate-service-account \ 46 | --key-file "${GOOGLE_APPLICATION_CREDENTIALS}" \ 47 | --project $GOOGLE_CLOUD_PROJECT 48 | fi 49 | 50 | # Only run Deployment Tests on nightly builds 51 | if [ "$PULL_REQUEST_NUMBER" != "" ]; then 52 | export RUN_DEPLOYMENT_TESTS="" 53 | fi 54 | 55 | # Install google/cloud-tools globally 56 | composer global require google/cloud-tools:dev-main 57 | 58 | # Install composer in all directories containing composer.json 59 | find . -name composer.json -not -path '*vendor/*' -exec dirname {} \; | while read COMPOSER_DIR 60 | do 61 | pushd $COMPOSER_DIR; 62 | composer install --quiet; 63 | popd; 64 | done 65 | 66 | # Run the tests in each of the sample directories 67 | find . -name 'phpunit*.xml*' -not -path '*vendor/*' | while read PHPUNIT_FILE 68 | do 69 | pushd $(dirname $PHPUNIT_FILE); 70 | phpunit -v -c $(basename $PHPUNIT_FILE); 71 | popd; 72 | done 73 | 74 | # If this is a periodic build, send the test log to the Build Cop Bot. 75 | # See https://github.com/googleapis/repo-automation-bots/tree/master/packages/buildcop. 76 | if [[ $KOKORO_BUILD_ARTIFACTS_SUBDIR = *"periodic"* ]]; then 77 | chmod +x $KOKORO_GFILE_DIR/linux_amd64/buildcop 78 | $KOKORO_GFILE_DIR/linux_amd64/buildcop 79 | fi 80 | -------------------------------------------------------------------------------- /.kokoro/trampoline.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Copyright 2020 Google LLC 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | 17 | python3 "${KOKORO_GFILE_DIR}/trampoline_v1.py" 18 | -------------------------------------------------------------------------------- /.php_cs.dist: -------------------------------------------------------------------------------- 1 | setRules([ 5 | '@PSR2' => true, 6 | 'concat_space' => ['spacing' => 'one'], 7 | 'no_unused_imports' => true, 8 | 'method_argument_space' => false, 9 | ]) 10 | ->setFinder( 11 | PhpCsFixer\Finder::create() 12 | ->in(__DIR__) 13 | ) 14 | ; 15 | -------------------------------------------------------------------------------- /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 | 8 | # The php-admins team is the default owner for anything not 9 | # explicitly taken by someone else. 10 | * @GoogleCloudPlatform/php-admins 11 | -------------------------------------------------------------------------------- /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 | 1. Submit a pull request. 36 | 37 | ### Run the tests 38 | 39 | Set up [application default credentials](https://cloud.google.com/docs/authentication/getting-started) 40 | by setting the environment variable `GOOGLE_APPLICATION_CREDENTIALS` to the 41 | path to a service account key JSON file and `GOOGLE_CLOUD_PROJECT` to your 42 | Google Cloud project ID: 43 | 44 | ``` 45 | export GOOGLE_APPLICATION_CREDENTIALS=/path/to/your/credentials.json 46 | export GOOGLE_CLOUD_PROJECT=YOUR_PROJECT_ID 47 | ``` 48 | 49 | These tests use `phpunit/phpunit:^7`. You can install this with composer 50 | globally: 51 | 52 | ``` 53 | composer global require phpunit/phpunit:^7 54 | ``` 55 | 56 | These tests also use `google/cloud-tools:dev-main`. You can install this with 57 | composer globally: 58 | 59 | ``` 60 | composer global require google/cloud-tools:dev-main 61 | ``` 62 | 63 | Now you can run the tests in the samples directory! 64 | 65 | ``` 66 | cd $SAMPLES_DIRECTORY 67 | phpunit 68 | ``` 69 | 70 | Use `phpunit -v` to get a more detailed output if there are errors. 71 | 72 | ## Style 73 | 74 | Samples in this repository follow the [PSR2][psr2] and [PSR4][psr4] 75 | recommendations. This is enforced using [PHP CS Fixer][php-cs-fixer]. 76 | 77 | Install that by running 78 | 79 | ``` 80 | composer global require friendsofphp/php-cs-fixer 81 | ``` 82 | 83 | Then to fix your directory or file run 84 | 85 | ``` 86 | php-cs-fixer fix . 87 | php-cs-fixer fix path/to/file 88 | ``` 89 | 90 | [psr2]: http://www.php-fig.org/psr/psr-2/ 91 | [psr4]: http://www.php-fig.org/psr/psr-4/ 92 | [php-cs-fixer]: https://github.com/FriendsOfPHP/PHP-CS-Fixer 93 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Getting started with PHP on Google Cloud Platform 2 | 3 | This repository contains the code for getting started with PHP on Google Cloud 4 | Platform. Please follow the tutorials for instructions on using these samples. 5 | 6 | * [Getting started with PHP][getting-started] 7 | * [Authenticating users on PHP][authenticate-users] 8 | * [Compute Engine for PHP][getting-started-gce] 9 | * [Session handling with PHP][sessions] 10 | * [Background Processing for PHP][background-processing] 11 | 12 | ## Google Cloud Samples 13 | 14 | To browse ready to use code samples check [Google Cloud Samples](https://cloud.google.com/docs/samples). 15 | 16 | ## Contributing changes 17 | 18 | * See [CONTRIBUTING.md](CONTRIBUTING.md) 19 | 20 | ## Licensing 21 | 22 | * See [LICENSE](LICENSE) 23 | 24 | [travis-badge]: https://travis-ci.org/GoogleCloudPlatform/getting-started-php.svg?branch=master 25 | [travis-link]: https://travis-ci.org/GoogleCloudPlatform/getting-started-php 26 | [getting-started]: http://cloud.google.com/php/getting-started 27 | [authenticate-users]: http://cloud.google.com/php/getting-started/authenticate-users 28 | [getting-started-gce]: https://cloud.google.com/php/tutorials/getting-started-on-compute-engine 29 | [sessions]: http://cloud.google.com/php/getting-started/sessions 30 | [background-processing]: https://cloud.google.com/php/getting-started/background-processing 31 | -------------------------------------------------------------------------------- /authenticate-users/.gitignore: -------------------------------------------------------------------------------- 1 | vendor 2 | composer.lock 3 | -------------------------------------------------------------------------------- /authenticate-users/README.md: -------------------------------------------------------------------------------- 1 | # Authenticating users with PHP on Google Cloud Platform 2 | 3 | This directory contains the complete sample code for authenticating users with 4 | PHP on Google Cloud Platform. Follow the tutorial to run the code: 5 | 6 | * [Authenticating users on PHP][authenticate-users] 7 | 8 | [authenticate-users]: http://cloud.google.com/php/getting-started/authenticate-users 9 | -------------------------------------------------------------------------------- /authenticate-users/app.yaml: -------------------------------------------------------------------------------- 1 | runtime: php73 2 | -------------------------------------------------------------------------------- /authenticate-users/composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "require": { 3 | "php": ">=7.1", 4 | "google/auth": "^1.9", 5 | "google/cloud-core": "^1.32", 6 | "kelvinmo/simplejwt": "^0.4.0" 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /authenticate-users/index.php: -------------------------------------------------------------------------------- 1 | verify($idToken, [ 37 | 'certsLocation' => Google\Auth\AccessToken::IAP_CERT_URL, 38 | 'throwException' => true, 39 | ]); 40 | 41 | if ($audience != $info['aud'] ?? '') { 42 | throw new Exception(sprintf( 43 | 'Audience %s did not match expected %s', $info['aud'], $audience 44 | )); 45 | } 46 | 47 | return [$info['email'], $info['sub']]; 48 | } 49 | # [END getting_started_auth_validate_assertion] 50 | 51 | # [START getting_started_auth_front_controller] 52 | /** 53 | * This is an example of a front controller for a flat file PHP site. Using a 54 | * static list provides security against URL injection by default. 55 | */ 56 | switch (@parse_url($_SERVER['REQUEST_URI'])['path']) { 57 | case '/': 58 | if (!Google\Auth\Credentials\GCECredentials::onGce()) { 59 | throw new Exception('You must deploy to appengine to run this sample'); 60 | } 61 | # [START getting_started_auth_audience] 62 | $metadata = new Google\Cloud\Core\Compute\Metadata(); 63 | $audience = sprintf( 64 | '/projects/%s/apps/%s', 65 | $metadata->getNumericProjectId(), 66 | $metadata->getProjectId() 67 | ); 68 | # [END getting_started_auth_audience] 69 | $idToken = getallheaders()['X-Goog-Iap-Jwt-Assertion'] ?? ''; 70 | try { 71 | list($email, $id) = validate_assertion($idToken, $audience); 72 | printf("

Hello %s

", $email); 73 | } catch (Exception $e) { 74 | printf('Failed to validate assertion: %s', $e->getMessage()); 75 | } 76 | break; 77 | case '': break; // Nothing to do, we're running our tests 78 | default: 79 | http_response_code(404); 80 | exit('Not Found'); 81 | } 82 | # [END getting_started_auth_front_controller] 83 | # [END getting_started_auth_all] 84 | -------------------------------------------------------------------------------- /authenticate-users/phpunit.xml.dist: -------------------------------------------------------------------------------- 1 | 2 | 17 | 18 | 19 | 20 | test 21 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /authenticate-users/test/DeployTest.php: -------------------------------------------------------------------------------- 1 | push(ApplicationDefaultCredentials::getIdTokenMiddleware( 70 | self::$iapClientId 71 | )); 72 | 73 | // create the HTTP client 74 | $this->client = new Client([ 75 | 'handler' => $stack, 76 | 'auth' => 'google_auth', 77 | 'base_uri' => self::getBaseUri(), 78 | ]); 79 | } 80 | 81 | public function testIndex() 82 | { 83 | $serviceAccountEmail = json_decode(file_get_contents( 84 | self::requireEnv('GOOGLE_APPLICATION_CREDENTIALS') 85 | ), true)['client_email']; 86 | $resp = $this->client->get('/'); 87 | $this->assertEquals('200', $resp->getStatusCode()); 88 | $this->assertContains( 89 | sprintf('

Hello %s

', $serviceAccountEmail), 90 | (string) $resp->getBody() 91 | ); 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /authenticate-users/test/indexTest.php: -------------------------------------------------------------------------------- 1 | router->group([ 48 | ], function ($router) { 49 | require __DIR__ . '/routes/web.php'; 50 | }); 51 | # [END getting_started_background_app_main] 52 | 53 | /* 54 | |-------------------------------------------------------------------------- 55 | | Run The Application 56 | |-------------------------------------------------------------------------- 57 | | 58 | | Once we have the application, we can handle the incoming request 59 | | through the kernel, and send the associated response back to 60 | | the client's browser allowing them to enjoy the creative 61 | | and wonderful application we have prepared for them. 62 | | 63 | */ 64 | 65 | // Unit testing uses the application object for request injection. 66 | if (getenv('PHPUNIT_TESTS') === '1') { 67 | return $app; 68 | } 69 | 70 | # [START getting_started_background_app_main] 71 | $app->run(); 72 | # [END getting_started_background_app_main] 73 | -------------------------------------------------------------------------------- /background-processing/app/php.ini: -------------------------------------------------------------------------------- 1 | ; Enable the gRPC and protobuf extensions 2 | extension=grpc.so 3 | extension=protobuf.so 4 | -------------------------------------------------------------------------------- /background-processing/app/resources/views/home.php: -------------------------------------------------------------------------------- 1 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | Translations 24 | 25 | 26 | 27 | 28 | 29 | 65 | 73 | 74 | 75 | 76 | 77 |
78 |
79 |
80 | 81 | Translate with Background Processing 82 |
83 |
84 |
85 |
86 |
87 |
88 |
89 |
90 |
91 | 92 | 93 |
94 | 102 | 104 |
105 |
106 |
107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 123 | 129 | 130 | 131 | 132 |
OriginalTranslation
118 | 119 | 120 | 121 | 122 | 124 | 125 | 126 | 127 | 128 |
133 |
134 | 135 |
136 |
137 |
138 |
139 |
140 | 141 |
142 |
143 |
144 | 145 | 146 | -------------------------------------------------------------------------------- /background-processing/app/routes/web.php: -------------------------------------------------------------------------------- 1 | get('/', function (Request $request) use ($projectId) { 31 | $firestore = new FirestoreClient([ 32 | 'projectId' => $projectId, 33 | ]); 34 | $translations = $firestore->collection('translations')->documents(); 35 | return view('home', ['translations' => $translations]); 36 | }); 37 | # [END getting_started_background_app_list] 38 | 39 | # [START getting_started_background_app_request] 40 | /** 41 | * Endpoint which publishes a PubSub request for a new translation. 42 | */ 43 | $router->post('/request-translation', function (Request $request) use ($projectId) { 44 | $acceptableLanguages = ['de', 'en', 'es', 'fr', 'ja', 'sw']; 45 | if (!in_array($lang = $request->get('lang'), $acceptableLanguages)) { 46 | throw new Exception('Unsupported Language: ' . $lang); 47 | } 48 | if (!$text = $request->get('v')) { 49 | throw new Exception('No text to translate'); 50 | } 51 | $pubsub = new PubSubClient([ 52 | 'projectId' => $projectId, 53 | ]); 54 | $topic = $pubsub->topic('translate'); 55 | $topic->publish(['data' => json_encode([ 56 | 'language' => $lang, 57 | 'text' => $text, 58 | ])]); 59 | 60 | return ''; 61 | }); 62 | # [END getting_started_background_app_request] 63 | -------------------------------------------------------------------------------- /background-processing/backend/.dockerignore: -------------------------------------------------------------------------------- 1 | Dockerfile 2 | vendor/ 3 | -------------------------------------------------------------------------------- /background-processing/backend/.gitignore: -------------------------------------------------------------------------------- 1 | vendor/ 2 | -------------------------------------------------------------------------------- /background-processing/backend/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM gcr.io/gae-runtimes/php73:php73_20190827_7_3_7_RC03 2 | 3 | WORKDIR /srv/ 4 | 5 | # NOTE: The entrypoint "/start", which starts up NGINX and PHP-FPM, 6 | # is configured by creating a `.googleconfig/app_start.json` file with the 7 | # contents: 8 | # 9 | # {"entrypointContents": "CUSTOM_ENTRYPOINT"} 10 | # 11 | # We configure it to use the `router.php` file included in this package. 12 | RUN mkdir .googleconfig && \ 13 | echo '{"entrypointContents": "serve vendor/bin/router.php"}' > .googleconfig/app_start.json 14 | 15 | # Copy over composer files and run "composer install" 16 | COPY composer.* php.ini ./ 17 | COPY --from=composer:1 /usr/bin/composer /usr/local/bin 18 | RUN composer install --no-dev 19 | 20 | # Copy over all application files 21 | COPY . . 22 | 23 | # Set a runtime name 24 | ENV GAE_RUNTIME=php73 \ 25 | # Set the function to use 26 | FUNCTION_TARGET=translateString \ 27 | # This function will respond to Pub/Sub events 28 | FUNCTION_SIGNATURE_TYPE=event 29 | -------------------------------------------------------------------------------- /background-processing/backend/composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "require": { 3 | "google/cloud-translate": "^1.4", 4 | "google/cloud-firestore": "^1.7", 5 | "google/cloud-functions-framework": "^0.7.0" 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /background-processing/backend/index.php: -------------------------------------------------------------------------------- 1 | $data['text'], 33 | 'lang' => $data['language'], 34 | ]; 35 | # [END getting_started_background_translate_init] 36 | 37 | # [START getting_started_background_translate_transaction] 38 | $docId = sprintf('%s:%s', $data['language'], base64_encode($data['text'])); 39 | $docRef = $firestore->collection('translations')->document($docId); 40 | 41 | $firestore->runTransaction( 42 | function (Transaction $transaction) use ($translate, $translation, $docRef) { 43 | $snapshot = $transaction->snapshot($docRef); 44 | if ($snapshot->exists()) { 45 | return; // Do nothing if the document already exists 46 | } 47 | 48 | # [START getting_started_background_translate_string] 49 | $result = $translate->translate($translation['original'], [ 50 | 'target' => $translation['lang'], 51 | ]); 52 | # [END getting_started_background_translate_string] 53 | $transaction->set($docRef, $translation + [ 54 | 'translated' => $result['text'], 55 | 'originalLang' => $result['source'], 56 | ]); 57 | } 58 | ); 59 | # [END getting_started_background_translate_transaction] 60 | 61 | echo "Done."; 62 | } 63 | # [END getting_started_background_translate] 64 | -------------------------------------------------------------------------------- /background-processing/backend/php.ini: -------------------------------------------------------------------------------- 1 | ; Enable the gRPC and protobuf extensions 2 | extension=grpc.so 3 | extension=protobuf.so 4 | -------------------------------------------------------------------------------- /background-processing/test/app/AppTest.php: -------------------------------------------------------------------------------- 1 | call('GET', '/'); 44 | $this->assertEquals(200, $response->getStatusCode()); 45 | $this->assertContains( 46 | 'Translate with Background Processing', 47 | $response->getContent() 48 | ); 49 | $crawler = new Crawler($response->getContent()); 50 | $this->assertEquals(1, $crawler->selectButton('Submit')->count()); 51 | } 52 | 53 | public function testRequestTranslationWithInvalidLanguage() 54 | { 55 | // Try with no language parameter 56 | $response = $this->call('POST', '/request-translation'); 57 | $this->assertEquals(500, $response->getStatusCode()); 58 | $this->assertContains( 59 | 'Unsupported Language:', 60 | $response->getContent() 61 | ); 62 | 63 | $response = $this->call('POST', '/request-translation', [ 64 | 'lang'=> 'klingonese' 65 | ]); 66 | $this->assertEquals(500, $response->getStatusCode()); 67 | $this->assertContains( 68 | 'Unsupported Language: klingonese', 69 | $response->getContent() 70 | ); 71 | } 72 | 73 | public function testSubmitTranslation() 74 | { 75 | $response = $this->call('POST', '/request-translation', [ 76 | 'lang' => 'en', 77 | 'v' => 'This is a test translation', 78 | ]); 79 | $this->assertEquals(200, $response->getStatusCode()); 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /background-processing/test/backend/translateTest.php: -------------------------------------------------------------------------------- 1 | $text, 30 | 'language' => 'es', 31 | ]; 32 | $result = translateString($data); 33 | 34 | $firestore = new FirestoreClient(); 35 | $docRef = $firestore->collection('translations')->document('es:' . base64_encode($text)); 36 | 37 | $this->assertTrue($docRef->snapshot()->exists()); 38 | 39 | $this->assertEquals($text, $docRef->snapshot()['original']); 40 | $this->assertEquals('en', $docRef->snapshot()['originalLang']); 41 | $this->assertEquals('es', $docRef->snapshot()['lang']); 42 | $this->assertContains('la vida loca', $docRef->snapshot()['translated']); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /background-processing/test/phpunit-app.xml.dist: -------------------------------------------------------------------------------- 1 | 2 | 17 | 18 | 19 | 20 | app 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /background-processing/test/phpunit-backend.xml.dist: -------------------------------------------------------------------------------- 1 | 2 | 17 | 18 | 19 | 20 | backend 21 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /background-processing/test/phpunit-system.xml.dist: -------------------------------------------------------------------------------- 1 | 2 | 17 | 18 | 19 | 20 | system 21 | 22 | 23 | 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /background-processing/test/system/DeployTest.php: -------------------------------------------------------------------------------- 1 | $projectId])) 66 | ->topic('translate') 67 | ->subscription('test-' . $versionId); 68 | self::$image = sprintf('gcr.io/%s/%s-image', $projectId, $versionId); 69 | } 70 | 71 | private static function beforeDeploy() 72 | { 73 | // Ensure setUpDeploymentVars has been called 74 | if (is_null(self::$frontend)) { 75 | self::setUpDeploymentVars(); 76 | } 77 | 78 | $frontendDir = FileUtil::cloneDirectoryIntoTmp(__DIR__ . '/../../app'); 79 | self::$frontend->setDir($frontendDir); 80 | 81 | $backendDir = FileUtil::cloneDirectoryIntoTmp(__DIR__ . '/../../backend'); 82 | self::$backend->setDir($backendDir); 83 | } 84 | 85 | private static function doDeploy() 86 | { 87 | // Deploy both the frontend and backend to App Engine. 88 | if (false === self::$frontend->deploy()) { 89 | return false; 90 | } 91 | 92 | if (false === self::$backend->build(self::$image)) { 93 | return false; 94 | } 95 | 96 | if (false === self::$backend->deploy(self::$image)) { 97 | return false; 98 | } 99 | 100 | if (self::$subscription->exists()) { 101 | self::$subscription->delete(); 102 | } 103 | 104 | $serviceAccountJson = json_decode(file_get_contents( 105 | self::requireEnv('GOOGLE_APPLICATION_CREDENTIALS') 106 | ), true); 107 | 108 | // Create the pubsub subscription 109 | self::$subscription->create([ 110 | 'pushConfig' => [ 111 | 'pushEndpoint' => self::$backend->getBaseUrl(), 112 | 'oidcToken' => [ 113 | 'serviceAccountEmail' => $serviceAccountJson['client_email'] 114 | ] 115 | ], 116 | ]); 117 | 118 | return true; 119 | } 120 | 121 | /** 122 | * Delete a deployed App Engine app. 123 | */ 124 | private static function doDelete() 125 | { 126 | self::$frontend->delete(); 127 | self::$backend->delete(); 128 | self::$backend->deleteImage(self::$image); 129 | self::$subscription->delete(); 130 | } 131 | 132 | public function testFrontend() 133 | { 134 | $resp = $this->client->get('/'); 135 | $this->assertEquals('200', $resp->getStatusCode()); 136 | $this->assertContains( 137 | 'Translate with Background Processing', 138 | (string) $resp->getBody() 139 | ); 140 | } 141 | 142 | public function testBackend() 143 | { 144 | // Get the ID Token from the Google OAuth2 endpoint 145 | $targetAudience = self::$backend->getBaseUrl(); 146 | $jsonKey = CredentialsLoader::fromEnv(); 147 | $auth = new OAuth2([ 148 | 'audience' => CredentialsLoader::TOKEN_CREDENTIAL_URI, 149 | 'issuer' => $jsonKey['client_email'], 150 | 'signingAlgorithm' => 'RS256', 151 | 'signingKey' => $jsonKey['private_key'], 152 | 'tokenCredentialUri' => CredentialsLoader::TOKEN_CREDENTIAL_URI, 153 | 'additionalClaims' => ['target_audience' => $targetAudience], 154 | ]); 155 | $token = $auth->fetchAuthToken(); 156 | 157 | // Send an authenticated HTTP request to the backend 158 | $timestamp = time(); 159 | $text = 'Test sent directly to the backend ' . $timestamp; 160 | $backendClient = new Client(); 161 | $resp = $backendClient->post($targetAudience, [ 162 | 'json' => [ 163 | 'message' => [ 164 | 'data' => base64_encode(json_encode([ 165 | 'language' => 'es', 166 | 'text' => $text, 167 | ])), 168 | ] 169 | ], 170 | 'headers' => ['Authorization' => 'Bearer ' . $token['id_token']], 171 | ]); 172 | $this->assertEquals('200', $resp->getStatusCode()); 173 | $this->assertContains('Done.', (string) $resp->getBody()); 174 | 175 | // Verify the translation exists in Firestore 176 | $firestore = new FirestoreClient(); 177 | $docRef = $firestore->collection('translations') 178 | ->document('es:' . base64_encode($text)); 179 | 180 | $this->assertTrue($docRef->snapshot()->exists()); 181 | $this->assertEquals($text, $docRef->snapshot()['original']); 182 | $this->assertEquals('en', $docRef->snapshot()['originalLang']); 183 | $this->assertEquals('es', $docRef->snapshot()['lang']); 184 | $this->assertContains((string) $timestamp, $docRef->snapshot()['translated']); 185 | 186 | $docRef->delete(); 187 | } 188 | 189 | /** 190 | * @depends testFrontend 191 | * @depends testBackend 192 | */ 193 | public function testRequestTranslation() 194 | { 195 | $timestamp = time(); 196 | $resp = $this->client->post('/request-translation?lang=es', [ 197 | 'form_params' => ['v' => 'Living the crazy life ' . $timestamp], 198 | ]); 199 | 200 | $this->assertEquals('200', $resp->getStatusCode()); 201 | $this->runEventuallyConsistentTest(function () use ($timestamp) { 202 | $resp = $this->client->get('/'); 203 | $this->assertContains( 204 | 'Viviendo la vida loca ' . $timestamp, 205 | (string) $resp->getBody() 206 | ); 207 | }); 208 | } 209 | 210 | public function getBaseUri() 211 | { 212 | return self::$frontend->getBaseUrl(); 213 | } 214 | } 215 | -------------------------------------------------------------------------------- /bookshelf/.gitignore: -------------------------------------------------------------------------------- 1 | /vendor 2 | /.idea 3 | Homestead.json 4 | Homestead.yaml 5 | storage/logs/* 6 | .env 7 | -------------------------------------------------------------------------------- /bookshelf/README.md: -------------------------------------------------------------------------------- 1 | # Getting Started with PHP on Google Cloud Platform 2 | 3 | This directory contains the code for deploying a basic PHP app to Google Cloud 4 | Platform. Follow the tutorial to use this sample. 5 | 6 | * [Getting started with PHP][getting-started] 7 | 8 | [getting-started]: http://cloud.google.com/php/getting-started 9 | -------------------------------------------------------------------------------- /bookshelf/app.yaml: -------------------------------------------------------------------------------- 1 | runtime: php73 2 | 3 | env_variables: 4 | APP_DEBUG: true 5 | LOG_CHANNEL: stderr 6 | APP_STORAGE: /tmp 7 | -------------------------------------------------------------------------------- /bookshelf/app/Exceptions/Handler.php: -------------------------------------------------------------------------------- 1 | singleton( 56 | Illuminate\Contracts\Debug\ExceptionHandler::class, 57 | App\Exceptions\Handler::class 58 | ); 59 | 60 | /* 61 | |-------------------------------------------------------------------------- 62 | | Load The Application Routes 63 | |-------------------------------------------------------------------------- 64 | | 65 | | Next we will include the routes file so that they can all be added to 66 | | the application. This will provide all of the URLs the application 67 | | can respond to, as well as the controllers that may handle them. 68 | | 69 | */ 70 | 71 | $app->router->group([ 72 | 'namespace' => 'App\Http\Controllers', 73 | ], function ($router) { 74 | require __DIR__ . '/routes/web.php'; 75 | }); 76 | 77 | /* 78 | |-------------------------------------------------------------------------- 79 | | Run The Application 80 | |-------------------------------------------------------------------------- 81 | | 82 | | Once we have the application, we can handle the incoming request 83 | | through the kernel, and send the associated response back to 84 | | the client's browser allowing them to enjoy the creative 85 | | and wonderful application we have prepared for them. 86 | | 87 | */ 88 | 89 | // Unit testing uses the application object for request injection. 90 | if (getenv('PHPUNIT_TESTS') === '1') { 91 | return $app; 92 | } 93 | 94 | $app->run(); 95 | -------------------------------------------------------------------------------- /bookshelf/php.ini: -------------------------------------------------------------------------------- 1 | ; enable the gRPC extension 2 | extension=grpc.so 3 | -------------------------------------------------------------------------------- /bookshelf/phpunit.xml.dist: -------------------------------------------------------------------------------- 1 | 2 | 17 | 18 | 19 | 20 | test 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /bookshelf/resources/views/base.php: -------------------------------------------------------------------------------- 1 | 17 | 18 | 26 | 27 | 28 | 29 | 30 | Bookshelf - PHP on Google Cloud Platform 31 | 32 | 33 | 34 | 35 | 36 | 46 |
47 | 48 |
49 | 50 | 51 | -------------------------------------------------------------------------------- /bookshelf/resources/views/form.php: -------------------------------------------------------------------------------- 1 | 21 | 22 | 30 | 31 |

book

32 | 33 |
34 | 35 |
36 | 37 | 38 |
39 | 40 |
41 | 42 | 43 |
44 | 45 |
46 | 47 | 48 |
49 | 50 |
51 | 52 | 53 |
54 | 55 |
56 | 57 | 58 |
59 | 60 | 64 | 65 | 66 |
67 | 68 | 70 | ob_get_clean() ]) ?> 71 | -------------------------------------------------------------------------------- /bookshelf/resources/views/list.php: -------------------------------------------------------------------------------- 1 | 21 | 22 | 33 | 34 |

Books

35 | 36 | 37 | Add book 38 | 39 | 40 | $book): ?> 41 |
42 | 43 | get('image_url')): ?> 44 |
45 | 46 |
47 | 48 |
49 |

get('title') ?>

50 |

get('author') ?>

51 |
52 |
53 |
54 | 55 | 56 |

No books found

57 | 58 | 63 | 64 | 65 | 67 | ob_get_clean() ]) ?> 68 | -------------------------------------------------------------------------------- /bookshelf/resources/views/show.php: -------------------------------------------------------------------------------- 1 | 21 | 22 | 29 | 30 |

Book

31 |
32 |
33 | 34 | 35 | Edit book 36 | 37 | 41 |
42 |
43 | 44 |
45 | get('image_url')): ?> 46 |
47 | 48 |
49 | 50 |
51 |

52 | get('title') ?> 53 | get('published_date') ?> 54 |

55 |
By get('author') ?: 'Unknown' ?>
56 |

get('description') ?>

57 |
58 |
59 | 60 | 62 | ob_get_clean() ]) ?> 63 | -------------------------------------------------------------------------------- /bookshelf/routes/web.php: -------------------------------------------------------------------------------- 1 | $projectId, 32 | ]); 33 | $collection = $firestore->collection($collectionName); 34 | # [END bookshelf_firestore_client] 35 | 36 | # [START bookshelf_cloud_storage_client] 37 | // Use the client library to call Cloud Storage 38 | use Google\Cloud\Storage\StorageClient; 39 | 40 | $storage = new StorageClient([ 41 | 'projectId' => $projectId, 42 | ]); 43 | $bucketId = $projectId . '_bucket'; 44 | $gcsBucket = $storage->bucket($bucketId); 45 | # [END bookshelf_cloud_storage_client] 46 | 47 | /* 48 | |-------------------------------------------------------------------------- 49 | | Application Routes 50 | |-------------------------------------------------------------------------- 51 | | 52 | | Here is where you can register all of the routes for an application. 53 | | It is a breeze. Simply tell Lumen the URIs it should respond to 54 | | and give it the Closure to call when that URI is requested. 55 | | 56 | */ 57 | 58 | /** 59 | * The Bookshelf homepage, displaying a paginated list of books. 60 | * 61 | * @param $collection The Firestore collection for retrieving the list of books. 62 | */ 63 | $router->get('/', function (Request $request) use ($collection) { 64 | $pageSize = getenv('BOOKSHELF_PAGE_SIZE') ?: 10; 65 | $query = $collection->limit($pageSize)->orderBy('title'); 66 | if ($token = $request->query->get('page_token')) { 67 | $lastBook = $collection->document($token)->snapshot(); 68 | $query = $query->startAfter($lastBook); 69 | } 70 | 71 | try { 72 | $books = $query->documents(); 73 | } catch (FailedPreconditionException $e) { 74 | // Firestore hasn't been enabled, catch the error gracefully. 75 | $books = []; 76 | } 77 | 78 | return view('list', [ 79 | 'books' => $books, 80 | 'pageSize' => $pageSize, 81 | ]); 82 | }); 83 | 84 | /** 85 | * A page displaying a form for creating a book. 86 | */ 87 | $router->get('/books/add', function () { 88 | return view('form', [ 89 | 'action' => 'Add', 90 | 'book' => null, 91 | ]); 92 | }); 93 | 94 | /** 95 | * Receives form data and creates a book object in the Firestore collection. 96 | * 97 | * @param $collection The Firestore collection for creating the book. 98 | * @param $gcsBucket The Storage Bucket object for storing the book cover image. 99 | * @param $bookFields Array of valid book fields. 100 | */ 101 | $router->post('/books/add', function (Request $request) use ($collection, $gcsBucket, $bookFields) { 102 | $bookData = $request->request->all(); 103 | 104 | // Validate the book data 105 | if ($invalid = array_diff_key($bookData, array_flip($bookFields))) { 106 | throw new \Exception('unsupported field: ' . implode(', ', array_keys($invalid))); 107 | } 108 | 109 | $image = $request->files->get('image'); 110 | if ($image && $image->isValid()) { 111 | $file = fopen($image->getRealPath(), 'r'); 112 | $object = $gcsBucket->upload($file, [ 113 | 'metadata' => ['contentType' => $image->getMimeType()], 114 | 'predefinedAcl' => 'publicRead', 115 | ]); 116 | $bookData['image_url'] = $object->info()['mediaLink']; 117 | } 118 | 119 | // Create the book 120 | $bookRef = $collection->newDocument(); 121 | $bookRef->set($bookData); 122 | 123 | return redirect('/books/' . $bookRef->id()); 124 | }); 125 | 126 | /** 127 | * Page for viewing the details of a specific book. 128 | * 129 | * @param $bookId The Firestore book ID. 130 | * @param $collection The Firestore collection for retrieving the book to view. 131 | */ 132 | $router->get('/books/{bookId}', function ($bookId) use ($collection) { 133 | # [START bookshelf_firestore_client_get_book] 134 | $bookRef = $collection->document($bookId); 135 | $snapshot = $bookRef->snapshot(); 136 | # [END bookshelf_firestore_client_get_book] 137 | 138 | if (!$snapshot->exists()) { 139 | return new Response('', Response::HTTP_NOT_FOUND); 140 | } 141 | 142 | return view('show', ['book' => $snapshot]); 143 | }); 144 | 145 | /** 146 | * Page for editing a specific book. 147 | * 148 | * @param $bookId The Firestore book ID. 149 | * @param $collection The Firestore collection for retrieving book to edit. 150 | */ 151 | $router->get('/books/{bookId}/edit', function ($bookId) use ($collection) { 152 | $bookRef = $collection->document($bookId); 153 | $snapshot = $bookRef->snapshot(); 154 | 155 | if (!$snapshot->exists()) { 156 | return new Response('', Response::HTTP_NOT_FOUND); 157 | } 158 | 159 | return view('form', [ 160 | 'action' => 'Edit', 161 | 'book' => $snapshot, 162 | ]); 163 | }); 164 | 165 | /** 166 | * Receives form data and edits a book object in the Firestore collection. 167 | * 168 | * @param $bookId The Firestore book ID. 169 | * @param $collection The Firestore collection for updating the book object. 170 | * @param $gcsBucket The Storage Bucket object for updating the book cover image. 171 | * @param $bookFields Array of valid book fields. 172 | */ 173 | $router->post('/books/{bookId}/edit', function (Request $request, $bookId) use ($collection, $gcsBucket, $bookFields) { 174 | $bookRef = $collection->document($bookId); 175 | $snapshot = $bookRef->snapshot(); 176 | 177 | if (!$snapshot->exists()) { 178 | return new Response('', Response::HTTP_NOT_FOUND); 179 | } 180 | 181 | // Get book data from the request object 182 | $bookData = $request->request->all(); 183 | $bookData['id'] = $bookId; 184 | 185 | // Validate the book data 186 | if ($invalid = array_diff_key($bookData, array_flip($bookFields))) { 187 | throw new \Exception('unsupported field: ' . implode(', ', array_keys($invalid))); 188 | } 189 | 190 | $image = $request->files->get('image'); 191 | if ($image && $image->isValid()) { 192 | $file = fopen($image->getRealPath(), 'r'); 193 | $object = $gcsBucket->upload($file, [ 194 | 'metadata' => ['contentType' => $image->getMimeType()], 195 | 'predefinedAcl' => 'publicRead', 196 | ]); 197 | $bookData['image_url'] = $object->info()['mediaLink']; 198 | } 199 | 200 | $bookRef->set($bookData, ['merge' => true]); 201 | return redirect('/books/' . $bookId); 202 | }); 203 | 204 | /** 205 | * Deletes the book object from the Firestore collection. 206 | * 207 | * @param $bookId The Firestore book ID. 208 | * @param $collection The Firestore collection for deleting the book object. 209 | * @param $gcsBucket The Storage Bucket object for deleting the book cover image. 210 | */ 211 | $router->post('/books/{bookId}/delete', function ($bookId) use ($collection, $gcsBucket) { 212 | $bookRef = $collection->document($bookId); 213 | $snapshot = $bookRef->snapshot(); 214 | 215 | if (!$snapshot->exists()) { 216 | return new Response('', Response::HTTP_NOT_FOUND); 217 | } 218 | 219 | $bookRef->delete(); 220 | 221 | if ($imageUrl = $snapshot->get('image_url')) { 222 | $components = explode('/', parse_url($imageUrl, PHP_URL_PATH)); 223 | $name = $components[count($components) - 1]; 224 | $object = $gcsBucket->object($name); 225 | $object->delete(); 226 | } 227 | 228 | return redirect('/', Response::HTTP_SEE_OTHER); 229 | }); 230 | 231 | /** 232 | * Sends a message of type INFO to Stackdriver Logging 233 | */ 234 | $router->get('/logs', function (Request $request) { 235 | $message = 'Hey, you triggered a custom log entry. Good job!'; 236 | $monolog = new Monolog\Logger('app'); 237 | $monolog->info($request->get('message') ?: $message); 238 | return redirect('/'); 239 | }); 240 | 241 | /** 242 | * Sends an exception to Stackdriver Error Reporting 243 | */ 244 | $router->get('/errors', function (Request $request) { 245 | $message = 'This is an intentional exception.'; 246 | throw new \Exception($request->get('message') ?: $message); 247 | }); 248 | -------------------------------------------------------------------------------- /bookshelf/test/ControllersTest.php: -------------------------------------------------------------------------------- 1 | self::$projectId, 58 | ]); 59 | self::$collection = $firestore->collection($collectionName); 60 | } 61 | 62 | /** @afterClass */ 63 | public static function deleteFirestoreCollection() 64 | { 65 | // Delete the collection 66 | foreach (self::$collection->documents() as $document) { 67 | printf('Deleting document %s' . PHP_EOL, $document->id()); 68 | $document->reference()->delete(); 69 | } 70 | } 71 | 72 | public function testIndex() 73 | { 74 | $response = $this->call('GET', '/'); 75 | $this->assertEquals(200, $response->getStatusCode()); 76 | $crawler = new Crawler($response->getContent()); 77 | $this->assertEquals(1, $crawler->selectLink('Add book')->count()); 78 | } 79 | 80 | public function testCreateBook() 81 | { 82 | $response = $this->call('GET', '/books/add'); 83 | $this->assertEquals(200, $response->getStatusCode()); 84 | 85 | // Fill the form and submit it 86 | $response = $this->call('POST', '/books/add', [ 87 | 'title' => 'The Cat in the Hat', 88 | 'author' => 'Dr. Suess', 89 | 'published_date' => '1957-01-01', 90 | 'description' => '', 91 | ], [], [ 92 | 'image' => new UploadedFile( 93 | __DIR__ . '/../images/moby-dick.jpg', 94 | 'moby-dick.jpg', 95 | 'image/jpg', 96 | null, 97 | true 98 | ), 99 | ]); 100 | $this->assertEquals(302, $response->getStatusCode()); 101 | $crawler = new Crawler($response->getContent()); 102 | $redirectUri = $crawler->filter('a')->attr('href'); 103 | $response = $this->call('GET', $redirectUri); 104 | 105 | $this->assertContains( 106 | 'moby-dick.jpg', 107 | $response->getContent() 108 | ); 109 | } 110 | 111 | public function testCreateBookWithInvalidFields() 112 | { 113 | $response = $this->call('POST', '/books/add', [ 114 | 'title' => 'The Cat in the Hat', 115 | 'invalid_field' => 'abc', 116 | ]); 117 | $this->assertEquals(500, $response->getStatusCode()); 118 | $this->assertContains('unsupported field: invalid_field', $response->getContent()); 119 | } 120 | 121 | /** 122 | * @depends testCreateBook 123 | */ 124 | public function testPagination() 125 | { 126 | // Create another book 127 | $response = $this->call('POST', '/books/add', [ 128 | 'title' => 'Treasure Island', 129 | 'author' => 'Robert Louis Stevenson', 130 | 'published_date' => '1883-01-01', 131 | 'description' => '', 132 | 'image_url' => '', 133 | ]); 134 | $this->assertEquals(302, $response->getStatusCode()); 135 | 136 | // Now go through the pages one by one and confirm we have the books 137 | $response = $this->call('GET', '/'); 138 | $this->assertEquals(200, $response->getStatusCode()); 139 | $crawler = new Crawler($response->getContent()); 140 | 141 | $this->assertEquals(1, $crawler 142 | ->filter('h4:contains("The Cat in the Hat")')->count()); 143 | $more = $crawler->filter('a:contains("More")'); 144 | $this->assertEquals(1, $more->count()); 145 | 146 | $response = $this->call('GET', $more->attr('href')); 147 | $this->assertEquals(200, $response->getStatusCode()); 148 | $crawler = new Crawler($response->getContent()); 149 | 150 | $this->assertEquals(1, $crawler 151 | ->filter('h4:contains("Treasure Island")')->count()); 152 | } 153 | 154 | /** 155 | * @depends testCreateBook 156 | */ 157 | public function testEditBook() 158 | { 159 | $response = $this->call('GET', '/'); 160 | $this->assertEquals(200, $response->getStatusCode()); 161 | $crawler = new Crawler($response->getContent()); 162 | 163 | // Find the first book and get the edit URL 164 | $firstBook = $crawler->filter('a:contains("The Cat in the Hat")'); 165 | $this->assertEquals(1, $firstBook->count()); 166 | 167 | // Edit the book 168 | $response = $this->call('POST', $firstBook->attr('href') . '/edit', [ 169 | 'title' => 'The Cat in the Hat (edited)', 170 | 'description' => '**New Description**' 171 | ]); 172 | 173 | $this->assertEquals(302, $response->getStatusCode()); 174 | $crawler = new Crawler($response->getContent()); 175 | $redirectUri = $crawler->filter('a')->attr('href'); 176 | $response = $this->call('GET', $redirectUri); 177 | 178 | $this->assertContains('The Cat in the Hat (edited)', $response->getContent()); 179 | $this->assertContains('**New Description**', $response->getContent()); 180 | } 181 | 182 | public function testLogging() 183 | { 184 | $response = $this->call('GET', '/logs'); 185 | $this->assertEquals(302, $response->getStatusCode()); 186 | 187 | $response = $this->call('GET', '/errors'); 188 | $this->assertEquals(500, $response->getStatusCode()); 189 | $this->assertContains('This is an intentional exception.', $response->getContent()); 190 | } 191 | } 192 | -------------------------------------------------------------------------------- /bookshelf/test/DeployTest.php: -------------------------------------------------------------------------------- 1 | setDir($tmpDir); 41 | chdir($tmpDir); 42 | } 43 | 44 | public function testIndex() 45 | { 46 | $resp = $this->client->get('/'); 47 | $this->assertEquals('200', $resp->getStatusCode()); 48 | $this->assertContains('

Books

', (string) $resp->getBody()); 49 | } 50 | 51 | public function testLogging() 52 | { 53 | $this->eventuallyConsistentRetryCount = 5; 54 | 55 | $logTimestamp = sprintf('%s-%s', time(), rand()); 56 | $message = 'test-logging-' . $logTimestamp; 57 | $resp = $this->client->get('/logs?message=' . $message); 58 | $this->assertEquals('200', $resp->getStatusCode()); 59 | 60 | $logging = new LoggingClient([ 61 | 'projectId' => self::$projectId, 62 | ]); 63 | 64 | $filter = sprintf( 65 | 'logName = "%s" AND timestamp >= "%s"', 66 | sprintf('projects/%s/logs/stderr', self::$projectId), 67 | date(\DateTime::RFC3339, strtotime('-1 minute')) 68 | ); 69 | 70 | $this->runEventuallyConsistentTest(function () use ($logging, $message, $filter) { 71 | $entries = $logging->entries(['filter' => $filter]); 72 | 73 | // Create a string from all the entry logs 74 | $logString = ''; 75 | foreach ($entries as $entry) { 76 | $info = $entry->info(); 77 | $logString .= $info['textPayload'] ?? implode(' ', $info['jsonPayload']); 78 | } 79 | 80 | $this->assertContains($message, $logString); 81 | }); 82 | } 83 | 84 | public function testErrorHandling() 85 | { 86 | $this->eventuallyConsistentRetryCount = 5; 87 | 88 | $logTimestamp = sprintf('%s-%s', time(), rand()); 89 | $message = 'test-error-handling-' . $logTimestamp; 90 | $resp = $this->client->get('/errors?message=' . $message, ['http_errors' => false]); 91 | $this->assertEquals('500', $resp->getStatusCode()); 92 | 93 | $errorStats = new ErrorStatsServiceClient(); 94 | $projectName = $errorStats->projectName(self::$projectId); 95 | $timeRange = (new QueryTimeRange()) 96 | ->setPeriod(Period::PERIOD_1_HOUR); 97 | 98 | // Iterate through all elements 99 | $this->runEventuallyConsistentTest(function () use ($errorStats, $projectName, $timeRange, $message) { 100 | $messages = []; 101 | $response = $errorStats->listGroupStats($projectName, [ 102 | 'timeRange' => $timeRange, 103 | 'pageSize' => 100, 104 | ]); 105 | foreach ($response->iterateAllElements() as $groupStat) { 106 | $response = $errorStats->listEvents($projectName, $groupStat->getGroup()->getGroupId(), [ 107 | 'timeRange' => $timeRange, 108 | 'pageSize' => 100, 109 | ]); 110 | foreach ($response->iterateAllElements() as $event) { 111 | $messages[] = $event->getMessage(); 112 | } 113 | } 114 | 115 | $this->assertContains($message, implode("\n", $messages)); 116 | }); 117 | } 118 | } 119 | -------------------------------------------------------------------------------- /gce/.gitignore: -------------------------------------------------------------------------------- 1 | /vendor 2 | /.idea 3 | Homestead.json 4 | Homestead.yaml 5 | storage/logs/* 6 | .env 7 | -------------------------------------------------------------------------------- /gce/README.md: -------------------------------------------------------------------------------- 1 | # Google Compute Engine for PHP 2 | 3 | This directory contains the complete sample code for deploying a "Hello, World!" 4 | app to Compute Engine with PHP. Follow the tutorial to run the sample. 5 | 6 | * [Compute Engine for PHP][getting-started-gce] 7 | 8 | [getting-started-gce]: https://cloud.google.com/php/tutorials/getting-started-on-compute-engine 9 | -------------------------------------------------------------------------------- /gce/composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "require": { 3 | "laravel/lumen-framework": " 5.8.*" 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /gce/config/fluentd/helloworld.conf: -------------------------------------------------------------------------------- 1 | 2 | type tail 3 | format none 4 | path /opt/app/storage/logs/*.log 5 | pos_file /var/tmp/fluentd.app.pos 6 | read_from_head true 7 | rotate_wait 10s 8 | tag helloworld 9 | 10 | 11 | -------------------------------------------------------------------------------- /gce/config/nginx/fastcgi_params: -------------------------------------------------------------------------------- 1 | # Copyright 2019 Google LLC 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | fastcgi_param QUERY_STRING $query_string; 16 | fastcgi_param REQUEST_METHOD $request_method; 17 | fastcgi_param CONTENT_TYPE $content_type; 18 | fastcgi_param CONTENT_LENGTH $content_length; 19 | 20 | fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name; 21 | fastcgi_param SCRIPT_NAME $fastcgi_script_name; 22 | fastcgi_param PATH_INFO $fastcgi_path_info; 23 | fastcgi_param PATH_TRANSLATED $document_root$fastcgi_path_info; 24 | fastcgi_param REQUEST_URI $request_uri; 25 | fastcgi_param DOCUMENT_URI $document_uri; 26 | fastcgi_param DOCUMENT_ROOT $document_root; 27 | fastcgi_param SERVER_PROTOCOL $server_protocol; 28 | 29 | fastcgi_param GATEWAY_INTERFACE CGI/1.1; 30 | fastcgi_param SERVER_SOFTWARE nginx/$nginx_version; 31 | 32 | fastcgi_param REMOTE_ADDR $http_x_real_ip; 33 | fastcgi_param REMOTE_PORT $remote_port; 34 | fastcgi_param SERVER_ADDR $server_addr; 35 | fastcgi_param SERVER_PORT $server_port; 36 | fastcgi_param SERVER_NAME $server_name; 37 | 38 | fastcgi_param HTTPS $https; 39 | 40 | # PHP only, required if PHP was built with --enable-force-cgi-redirect 41 | fastcgi_param REDIRECT_STATUS 200; 42 | -------------------------------------------------------------------------------- /gce/config/nginx/helloworld.conf: -------------------------------------------------------------------------------- 1 | # Copyright 2019 Google LLC 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | server { 16 | listen 80; 17 | root /opt/app; 18 | index index.html index.htm index.php; 19 | 20 | # Media: images, icons, video, audio, HTC 21 | location ~* \.(?:jpg|jpeg|gif|png|ico|cur|gz|svg|svgz|mp4|ogg|ogv|webm|htc)$ { 22 | expires 10m; 23 | access_log off; 24 | etag on; 25 | add_header Cache-Control "public"; 26 | } 27 | 28 | # CSS and Javascript 29 | location ~* \.(?:css|js)$ { 30 | expires 10m; 31 | access_log off; 32 | etag on; 33 | add_header Cache-Control "public"; 34 | } 35 | 36 | # pass the PHP scripts to FastCGI server 37 | location ~ \.php$ { 38 | try_files $uri =404; 39 | fastcgi_split_path_info ^(.+?\.php)(/.*)$; 40 | fastcgi_pass unix:/var/run/php/php7.2-fpm.sock; 41 | fastcgi_index index.php; 42 | include fastcgi_params; 43 | } 44 | 45 | location / { 46 | # try to serve file directly, fallback to front controller 47 | try_files $uri /index.php$is_args$args; 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /gce/index.php: -------------------------------------------------------------------------------- 1 | router->group([], function ($router) { 45 | require __DIR__ . '/routes/web.php'; 46 | }); 47 | 48 | /* 49 | |-------------------------------------------------------------------------- 50 | | Run The Application 51 | |-------------------------------------------------------------------------- 52 | | 53 | | Once we have the application, we can handle the incoming request 54 | | through the kernel, and send the associated response back to 55 | | the client's browser allowing them to enjoy the creative 56 | | and wonderful application we have prepared for them. 57 | | 58 | */ 59 | 60 | // Unit testing uses the application object for request injection. 61 | if (getenv('PHPUNIT_TESTS') === '1') { 62 | return $app; 63 | } 64 | 65 | $app->run(); 66 | -------------------------------------------------------------------------------- /gce/phpunit.xml.dist: -------------------------------------------------------------------------------- 1 | 2 | 17 | 18 | 19 | 20 | test 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /gce/routes/web.php: -------------------------------------------------------------------------------- 1 | get('/', function (Request $request) { 24 | return 'Hello, World!'; 25 | }); 26 | -------------------------------------------------------------------------------- /gce/scripts/deploy.sh: -------------------------------------------------------------------------------- 1 | # Copyright 2019 Google LLC 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | #! /bin/bash 15 | 16 | set -ex 17 | 18 | # [START getting_started_gce_create_instance] 19 | MY_INSTANCE_NAME='my-app-instance' 20 | ZONE=us-central1-f 21 | 22 | gcloud compute instances create $MY_INSTANCE_NAME \ 23 | --image-family=ubuntu-1804-lts \ 24 | --image-project=ubuntu-os-cloud \ 25 | --machine-type=g1-small \ 26 | --scopes userinfo-email,cloud-platform \ 27 | --metadata-from-file startup-script=scripts/startup-script.sh \ 28 | --zone $ZONE \ 29 | --tags http-server 30 | # [END getting_started_gce_create_instance] 31 | 32 | gcloud compute firewall-rules create default-allow-http-80 \ 33 | --allow tcp:80 \ 34 | --source-ranges 0.0.0.0/0 \ 35 | --target-tags http-server \ 36 | --description "Allow port 80 access to http-server" 37 | -------------------------------------------------------------------------------- /gce/scripts/startup-script.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Copyright 2019 Google LLC 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | 17 | # [START getting_started_gce_startup_script] 18 | set -e 19 | export HOME=/root 20 | 21 | # Install PHP and dependencies from apt 22 | apt-get update 23 | apt-get install -y git nginx php7.2 php7.2-fpm php7.2-mysql php7.2-dev \ 24 | php7.2-mbstring php7.2-zip php-pear pkg-config 25 | 26 | # Install Composer 27 | curl -sS https://getcomposer.org/installer | \ 28 | /usr/bin/php -- \ 29 | --install-dir=/usr/local/bin \ 30 | --filename=composer 31 | 32 | # Get the application source code 33 | git clone https://github.com/googlecloudplatform/getting-started-php /opt/src 34 | ln -s /opt/src/gce /opt/app 35 | 36 | # Run Composer 37 | composer install -d /opt/app --no-ansi --no-progress --no-dev 38 | 39 | # Disable the default NGINX configuration 40 | rm /etc/nginx/sites-enabled/default 41 | 42 | # Enable our NGINX configuration 43 | cp /opt/app/config/nginx/helloworld.conf /etc/nginx/sites-available/helloworld.conf 44 | ln -s /etc/nginx/sites-available/helloworld.conf /etc/nginx/sites-enabled/helloworld.conf 45 | cp /opt/app/config/nginx/fastcgi_params /etc/nginx/fastcgi_params 46 | 47 | # Start NGINX 48 | systemctl restart nginx.service 49 | 50 | # Install Fluentd 51 | curl -s "https://storage.googleapis.com/signals-agents/logging/google-fluentd-install.sh" | bash 52 | 53 | # Enable our Fluentd configuration 54 | cp /opt/app/config/fluentd/helloworld.conf /etc/google-fluentd/config.d/helloworld.conf 55 | 56 | # Start Fluentd 57 | service google-fluentd restart & 58 | # [END getting_started_gce_startup_script] 59 | -------------------------------------------------------------------------------- /gce/scripts/teardown.sh: -------------------------------------------------------------------------------- 1 | # Copyright 2019 Google LLC 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | #! /bin/bash 15 | 16 | set -x 17 | 18 | MY_INSTANCE_NAME='my-app-instance' 19 | ZONE=us-central1-f 20 | 21 | gcloud compute instances delete $MY_INSTANCE_NAME \ 22 | --zone=$ZONE --delete-disks=all 23 | 24 | gcloud compute firewall-rules delete default-allow-http-80 25 | -------------------------------------------------------------------------------- /gce/test/AppTest.php: -------------------------------------------------------------------------------- 1 | call('GET', '/'); 38 | $this->assertEquals(200, $response->getStatusCode()); 39 | $this->assertContains('Hello, World!', $response->getContent()); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /gce/test/DeployTest.php: -------------------------------------------------------------------------------- 1 | execute(function () { 63 | $cmd = sprintf( 64 | 'gcloud compute instances get-serial-port-output %s --zone=%s', 65 | self::$instanceName, 66 | self::$instanceZone 67 | ); 68 | exec($cmd, $output); 69 | $output = implode("\n", $output); 70 | if (false === strpos($output, 'Finished running startup scripts')) { 71 | echo "Waiting for startup script to complete...\n"; 72 | throw new \Exception('Startup script is still running'); 73 | } 74 | }); 75 | } 76 | 77 | private static function doDelete() 78 | { 79 | passthru('bash scripts/teardown.sh'); 80 | } 81 | 82 | public function testIndex() 83 | { 84 | $resp = $this->client->get('/'); 85 | $this->assertEquals('200', $resp->getStatusCode()); 86 | $this->assertContains('Hello, World!', (string) $resp->getBody()); 87 | } 88 | 89 | /** 90 | * Return the URI of the deployed App Engine app. 91 | */ 92 | private function getBaseUri() 93 | { 94 | $cmd = sprintf( 95 | 'gcloud compute instances describe %s --zone=%s | grep natIP | awk \'{print $2}\'', 96 | self::$instanceName, 97 | self::$instanceZone 98 | ); 99 | exec($cmd, $output); 100 | if (empty($output[0])) { 101 | throw new \Exception('Instance IP not found'); 102 | } 103 | return 'http://' . $output[0]; 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ 3 | "config:base", 4 | ":preserveSemverRanges", 5 | ":rebaseStalePrs" 6 | ] 7 | } 8 | -------------------------------------------------------------------------------- /sessions/.gitignore: -------------------------------------------------------------------------------- 1 | /vendor 2 | /.idea 3 | -------------------------------------------------------------------------------- /sessions/README.md: -------------------------------------------------------------------------------- 1 | # Session handling with PHP on Google Cloud Platform 2 | 3 | This directory contains the complete sample code for session handling with PHP 4 | using Firestore. Follow the tutorial to run the code: 5 | 6 | * [Session handling with PHP][sessions] 7 | 8 | [sessions]: http://cloud.google.com/php/getting-started/sessions 9 | -------------------------------------------------------------------------------- /sessions/app.yaml: -------------------------------------------------------------------------------- 1 | runtime: php73 2 | -------------------------------------------------------------------------------- /sessions/composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "require": { 3 | "google/cloud-firestore": "^1.9" 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /sessions/index.php: -------------------------------------------------------------------------------- 1 | $projectId, 28 | ]); 29 | 30 | # [START getting_started_sessions_create_handler] 31 | $handler = $firestore->sessionHandler(['gcLimit' => 500]); 32 | # [END getting_started_sessions_create_handler] 33 | 34 | // Configure PHP to use the the Firebase session handler. 35 | session_set_save_handler($handler, true); 36 | session_save_path('sessions'); 37 | session_start(); 38 | # [END getting_started_sessions_register_handler] 39 | 40 | # [START getting_started_sessions_front_controller] 41 | $colors = ['red', 'blue', 'green', 'yellow', 'pink']; 42 | /** 43 | * This is an example of a front controller for a flat file PHP site. Using a 44 | * Static list provides security against URL injection by default. 45 | */ 46 | switch (@parse_url($_SERVER['REQUEST_URI'])['path']) { 47 | case '/': 48 | if (!isset($_SESSION['views'])) { 49 | $_SESSION['views'] = 0; 50 | $_SESSION['color'] = $colors[rand(0, 4)]; 51 | } 52 | printf( 53 | 'Views: %s', 54 | $_SESSION['color'], 55 | $_SESSION['views']++ 56 | ); 57 | break; 58 | default: 59 | http_response_code(404); 60 | exit('Not Found'); 61 | } 62 | # [END getting_started_sessions_front_controller] 63 | # [END getting_started_sessions_all] 64 | -------------------------------------------------------------------------------- /sessions/php.ini: -------------------------------------------------------------------------------- 1 | ; enable the gRPC extension 2 | extension=grpc.so 3 | extension=protobuf.so 4 | -------------------------------------------------------------------------------- /sessions/phpunit.xml.dist: -------------------------------------------------------------------------------- 1 | 2 | 17 | 18 | 19 | 20 | test 21 | 22 | 23 | 24 | 25 | ./src 26 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /sessions/test/DeployTest.php: -------------------------------------------------------------------------------- 1 | client->get('/'); 34 | $this->assertEquals('200', $resp->getStatusCode()); 35 | $this->assertContains('Views: 0', (string) $resp->getBody()); 36 | 37 | $resp = $this->client->get('/'); 38 | $this->assertEquals('200', $resp->getStatusCode()); 39 | $this->assertContains('Views: 1', (string) $resp->getBody()); 40 | } 41 | 42 | /** 43 | * Set up the client. 44 | * 45 | * @before 46 | */ 47 | public function setUpClient() 48 | { 49 | $url = self::$gcloudWrapper->getBaseUrl(); 50 | $this->client = new Client([ 51 | 'base_uri' => $url, 52 | 'cookies' => true, 53 | ]); 54 | } 55 | } 56 | --------------------------------------------------------------------------------