├── .gitattributes ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── SECURITY.md ├── VERSION ├── bin └── google-cloud-batch ├── composer.json ├── perf-bootstrap.php ├── snippet-bootstrap.php ├── src ├── AnonymousCredentials.php ├── ApiHelperTrait.php ├── ArrayTrait.php ├── Batch │ ├── BatchDaemon.php │ ├── BatchDaemonTrait.php │ ├── BatchJob.php │ ├── BatchRunner.php │ ├── BatchTrait.php │ ├── ClosureSerializerInterface.php │ ├── ConfigStorageInterface.php │ ├── HandleFailureTrait.php │ ├── InMemoryConfigStorage.php │ ├── InterruptTrait.php │ ├── JobConfig.php │ ├── JobInterface.php │ ├── JobTrait.php │ ├── OpisClosureSerializer.php │ ├── ProcessItemInterface.php │ ├── QueueOverflowException.php │ ├── Retry.php │ ├── SerializableClientTrait.php │ ├── SimpleJob.php │ ├── SimpleJobTrait.php │ ├── SysvConfigStorage.php │ └── SysvProcessor.php ├── Blob.php ├── CallTrait.php ├── ClientTrait.php ├── Compute │ ├── Metadata.php │ └── Metadata │ │ └── Readers │ │ ├── HttpHandlerReader.php │ │ ├── ReaderInterface.php │ │ └── StreamReader.php ├── ConcurrencyControlTrait.php ├── DebugInfoTrait.php ├── DetectProjectIdTrait.php ├── Duration.php ├── EmulatorTrait.php ├── Exception │ ├── AbortedException.php │ ├── BadRequestException.php │ ├── ConflictException.php │ ├── DeadlineExceededException.php │ ├── FailedPreconditionException.php │ ├── GoogleException.php │ ├── NotFoundException.php │ ├── ServerException.php │ └── ServiceException.php ├── ExponentialBackoff.php ├── GeoPoint.php ├── GrpcRequestWrapper.php ├── GrpcTrait.php ├── Iam │ ├── Iam.php │ ├── IamConnectionInterface.php │ ├── IamManager.php │ └── PolicyBuilder.php ├── InsecureCredentialsWrapper.php ├── Int64.php ├── Iterator │ ├── ItemIterator.php │ ├── ItemIteratorTrait.php │ ├── PageIterator.php │ └── PageIteratorTrait.php ├── JsonTrait.php ├── Lock │ ├── FlockLock.php │ ├── LockInterface.php │ ├── LockTrait.php │ ├── SemaphoreLock.php │ └── SymfonyLockAdapter.php ├── Logger │ ├── AppEngineFlexFormatter.php │ ├── AppEngineFlexFormatterV2.php │ ├── AppEngineFlexFormatterV3.php │ ├── AppEngineFlexHandler.php │ ├── AppEngineFlexHandlerFactory.php │ ├── AppEngineFlexHandlerV2.php │ ├── AppEngineFlexHandlerV3.php │ └── FormatterTrait.php ├── LongRunning │ ├── LROTrait.php │ ├── LongRunningConnectionInterface.php │ ├── LongRunningOperation.php │ └── OperationResponseTrait.php ├── PhpArray.php ├── Report │ ├── CloudRunJobMetadataProvider.php │ ├── CloudRunMetadataProvider.php │ ├── CloudRunServiceMetadataProvider.php │ ├── EmptyMetadataProvider.php │ ├── GAEFlexMetadataProvider.php │ ├── GAEMetadataProvider.php │ ├── GAEStandardMetadataProvider.php │ ├── MetadataProviderInterface.php │ ├── MetadataProviderUtils.php │ └── SimpleMetadataProvider.php ├── RequestBuilder.php ├── RequestHandler.php ├── RequestProcessorTrait.php ├── RequestWrapper.php ├── RequestWrapperTrait.php ├── RestTrait.php ├── Retry.php ├── RetryDeciderTrait.php ├── ServiceBuilder.php ├── SysvTrait.php ├── Testing │ ├── ArrayHasSameValuesToken.php │ ├── CheckForClassTrait.php │ ├── DatastoreOperationRefreshTrait.php │ ├── FileListFilterIterator.php │ ├── GcTestListener.php │ ├── GrpcTestTrait.php │ ├── KeyPairGenerateTrait.php │ ├── Lock │ │ ├── MockGlobals.php │ │ └── MockValues.php │ ├── README.md │ ├── Reflection │ │ ├── DescriptionFactory.php │ │ ├── ReflectionHandlerFactory.php │ │ ├── ReflectionHandlerV5.php │ │ └── ReflectionHandlerV6.php │ ├── RegexFileFilter.php │ ├── Snippet │ │ ├── Container.php │ │ ├── Coverage │ │ │ ├── Coverage.php │ │ │ ├── ExcludeFilter.php │ │ │ ├── Scanner.php │ │ │ └── ScannerInterface.php │ │ ├── Fixtures.php │ │ ├── Parser │ │ │ ├── InvokeResult.php │ │ │ ├── Parser.php │ │ │ └── Snippet.php │ │ ├── SnippetTestCase.php │ │ └── keyfile-stub.json │ ├── StubTrait.php │ ├── System │ │ ├── DeletionQueue.php │ │ ├── KeyManager.php │ │ └── SystemTestCase.php │ └── TestHelpers.php ├── TimeTrait.php ├── Timestamp.php ├── TimestampTrait.php ├── Upload │ ├── AbstractUploader.php │ ├── MultipartUploader.php │ ├── ResumableUploader.php │ ├── SignedUrlUploader.php │ └── StreamableUploader.php ├── UriTrait.php ├── ValidateTrait.php ├── ValueMapperTrait.php └── WhitelistTrait.php ├── system-bootstrap.php └── unit-bootstrap.php /.gitattributes: -------------------------------------------------------------------------------- 1 | /*.xml.dist export-ignore 2 | /tests export-ignore 3 | /.github export-ignore 4 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Code of Conduct 2 | 3 | As contributors and maintainers of this project, 4 | and in the interest of fostering an open and welcoming community, 5 | we pledge to respect all people who contribute through reporting issues, 6 | posting feature requests, updating documentation, 7 | submitting pull requests or patches, and other activities. 8 | 9 | We are committed to making participation in this project 10 | a harassment-free experience for everyone, 11 | regardless of level of experience, gender, gender identity and expression, 12 | sexual orientation, disability, personal appearance, 13 | body size, race, ethnicity, age, religion, or nationality. 14 | 15 | Examples of unacceptable behavior by participants include: 16 | 17 | * The use of sexualized language or imagery 18 | * Personal attacks 19 | * Trolling or insulting/derogatory comments 20 | * Public or private harassment 21 | * Publishing other's private information, 22 | such as physical or electronic 23 | addresses, without explicit permission 24 | * Other unethical or unprofessional conduct. 25 | 26 | Project maintainers have the right and responsibility to remove, edit, or reject 27 | comments, commits, code, wiki edits, issues, and other contributions 28 | that are not aligned to this Code of Conduct. 29 | By adopting this Code of Conduct, 30 | project maintainers commit themselves to fairly and consistently 31 | applying these principles to every aspect of managing this project. 32 | Project maintainers who do not follow or enforce the Code of Conduct 33 | may be permanently removed from the project team. 34 | 35 | This code of conduct applies both within project spaces and in public spaces 36 | when an individual is representing the project or its community. 37 | 38 | Instances of abusive, harassing, or otherwise unacceptable behavior 39 | may be reported by opening an issue 40 | or contacting one or more of the project maintainers. 41 | 42 | This Code of Conduct is adapted from the [Contributor Covenant](http://contributor-covenant.org), version 1.2.0, 43 | available at [http://contributor-covenant.org/version/1/2/0/](http://contributor-covenant.org/version/1/2/0/) -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # How to Contribute 2 | 3 | We'd love to accept your patches and contributions to this project. We accept 4 | and review pull requests against the main 5 | [Google Cloud PHP](https://github.com/googleapis/google-cloud-php) 6 | repository, which contains all of our client libraries. You will also need to 7 | sign a Contributor License Agreement. For more details about how to contribute, 8 | see the 9 | [CONTRIBUTING.md](https://github.com/googleapis/google-cloud-php/blob/main/CONTRIBUTING.md) 10 | file in the main Google Cloud PHP repository. 11 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Google Cloud Core Libraries for PHP 2 | 3 | [![Latest Stable Version](https://poser.pugx.org/google/cloud-core/v/stable)](https://packagist.org/packages/google/cloud-core) [![Packagist](https://img.shields.io/packagist/dm/google/cloud-core.svg)](https://packagist.org/packages/google/cloud-core) 4 | 5 | * [API documentation](https://cloud.google.com/php/docs/reference/cloud-core/latest) 6 | 7 | **NOTE:** This repository is part of [Google Cloud PHP](https://github.com/googleapis/google-cloud-php). Any 8 | support requests, bug reports, or development contributions should be directed to 9 | that project. 10 | 11 | ### Installation 12 | 13 | **NOTE** This package is not intended for direct use. It provides common infrastructure 14 | to the rest of the Google Cloud PHP components. 15 | 16 | ```sh 17 | $ composer require google/cloud-core 18 | ``` 19 | 20 | ### Debugging 21 | 22 | Please see our [Debugging guide](https://github.com/googleapis/google-cloud-php/blob/main/DEBUG.md) 23 | for more information about the debugging tools. 24 | 25 | ### Version 26 | 27 | This component is considered GA (generally available). As such, it will not introduce backwards-incompatible changes in 28 | any minor or patch releases. We will address issues and requests with the highest priority. 29 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | # Security Policy 2 | 3 | To report a security issue, please use [g.co/vulnz](https://g.co/vulnz). 4 | 5 | The Google Security Team will respond within 5 working days of your report on g.co/vulnz. 6 | 7 | We use g.co/vulnz for our intake, and do coordination and disclosure here using GitHub Security Advisory to privately discuss and fix the issue. 8 | -------------------------------------------------------------------------------- /VERSION: -------------------------------------------------------------------------------- 1 | 1.62.3 2 | -------------------------------------------------------------------------------- /bin/google-cloud-batch: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env php 2 | run(); 53 | } else { 54 | $idNum = (int) $argv[2]; 55 | $job = $daemon->job($idNum); 56 | $job->run(); 57 | } 58 | } elseif ($argv[1] === 'retry') { 59 | $retry = new Retry(); 60 | $retry->retryAll(); 61 | } else { 62 | showUsageAndDie(); 63 | } 64 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "google/cloud-core", 3 | "description": "Google Cloud PHP shared dependency, providing functionality useful to all components.", 4 | "license": "Apache-2.0", 5 | "minimum-stability": "stable", 6 | "require": { 7 | "php": "^8.0", 8 | "rize/uri-template": "~0.3||~0.4", 9 | "google/auth": "^1.34", 10 | "guzzlehttp/guzzle": "^6.5.8||^7.4.4", 11 | "guzzlehttp/promises": "^1.4||^2.0", 12 | "guzzlehttp/psr7": "^2.6", 13 | "monolog/monolog": "^2.9||^3.0", 14 | "psr/http-message": "^1.0||^2.0", 15 | "google/gax": "^1.36.0" 16 | }, 17 | "require-dev": { 18 | "phpunit/phpunit": "^9.0", 19 | "phpspec/prophecy-phpunit": "^2.0", 20 | "squizlabs/php_codesniffer": "2.*", 21 | "phpdocumentor/reflection": "^5.3.3||^6.0", 22 | "phpdocumentor/reflection-docblock": "^5.3", 23 | "erusev/parsedown": "^1.6", 24 | "opis/closure": "^3", 25 | "google/cloud-common-protos": "~0.5" 26 | }, 27 | "suggest": { 28 | "opis/closure": "May be used to serialize closures to process jobs in the batch daemon. Please require version ^3.", 29 | "symfony/lock": "Required for the Spanner cached based session pool. Please require the following commit: 3.3.x-dev#1ba6ac9" 30 | }, 31 | "extra": { 32 | "component": { 33 | "id": "cloud-core", 34 | "target": "googleapis/google-cloud-php-core.git", 35 | "path": "Core", 36 | "entry": "src/ServiceBuilder.php" 37 | } 38 | }, 39 | "bin": [ 40 | "bin/google-cloud-batch" 41 | ], 42 | "autoload": { 43 | "psr-4": { 44 | "Google\\Cloud\\Core\\": "src" 45 | } 46 | }, 47 | "autoload-dev": { 48 | "psr-4": { 49 | "Google\\Cloud\\Core\\Tests\\": "tests" 50 | } 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /perf-bootstrap.php: -------------------------------------------------------------------------------- 1 | null 38 | ]; 39 | 40 | /** 41 | * Fetches the auth token. In this case it returns a null value. 42 | * 43 | * @param callable $httpHandler 44 | * @return array 45 | */ 46 | public function fetchAuthToken(?callable $httpHandler = null) 47 | { 48 | return $this->token; 49 | } 50 | 51 | /** 52 | * Returns the cache key. In this case it returns a null value, disabling 53 | * caching. 54 | * 55 | * @return string|null 56 | */ 57 | public function getCacheKey() 58 | { 59 | return null; 60 | } 61 | 62 | /** 63 | * Fetches the last received token. In this case, it returns the same null 64 | * auth token. 65 | * 66 | * @return array 67 | */ 68 | public function getLastReceivedToken() 69 | { 70 | return $this->token; 71 | } 72 | 73 | /** 74 | * This method has no effect for AnonymousCredentials. 75 | * 76 | * @param array $metadata metadata hashmap 77 | * @param string $authUri optional auth uri 78 | * @param callable $httpHandler callback which delivers psr7 request 79 | * @return array updated metadata hashmap 80 | */ 81 | public function updateMetadata( 82 | $metadata, 83 | $authUri = null, 84 | ?callable $httpHandler = null 85 | ) { 86 | return $metadata; 87 | } 88 | 89 | /** 90 | * This method always returns null for AnonymousCredentials. 91 | * 92 | * @return string|null 93 | */ 94 | public function getQuotaProject() 95 | { 96 | return null; 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /src/Batch/BatchDaemonTrait.php: -------------------------------------------------------------------------------- 1 | baseDir = getenv('GOOGLE_CLOUD_BATCH_DAEMON_FAILURE_DIR'); 46 | 47 | if ('false' === $this->baseDir) { 48 | // setting the file to the string "false" will prevent logging of failed items 49 | return; 50 | } 51 | 52 | if ($this->baseDir === false) { 53 | $this->baseDir = sprintf( 54 | '%s/batch-daemon-failure', 55 | sys_get_temp_dir() 56 | ); 57 | } 58 | 59 | if (!is_dir($this->baseDir) && !@mkdir($this->baseDir, 0700, true) && !is_dir($this->baseDir)) { 60 | throw new \RuntimeException( 61 | sprintf( 62 | 'Could not create a directory: %s', 63 | $this->baseDir 64 | ) 65 | ); 66 | } 67 | 68 | // Use getmypid for simplicity. 69 | $this->failureFile = sprintf( 70 | '%s/failed-items-%d', 71 | $this->baseDir, 72 | getmypid() 73 | ); 74 | } 75 | 76 | /** 77 | * Save the items to the failureFile. We silently abandon the items upon 78 | * failures in this method because there's nothing we can do. 79 | * 80 | * @param int $idNum A numeric id for the job. 81 | * @param array $items Items to save. 82 | */ 83 | public function handleFailure($idNum, array $items) 84 | { 85 | if (!$this->failureFile) { 86 | $this->initFailureFile(); 87 | } 88 | 89 | if ($this->failureFile) { 90 | $fp = @fopen($this->failureFile, 'a'); 91 | @fwrite($fp, json_encode(serialize([$idNum => $items])) . PHP_EOL); 92 | @fclose($fp); 93 | } 94 | } 95 | 96 | /** 97 | * Get all the filenames for the failure files. 98 | * 99 | * @return array Filenames for all the failure files. 100 | */ 101 | private function getFailedFiles() 102 | { 103 | $pattern = sprintf('%s/failed-items-*', $this->baseDir); 104 | return glob($pattern) ?: []; 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /src/Batch/InterruptTrait.php: -------------------------------------------------------------------------------- 1 | shutdown = true; 58 | break; 59 | } 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /src/Batch/JobConfig.php: -------------------------------------------------------------------------------- 1 | identifierToId) 56 | ? $this->jobs[$identifier] 57 | : null; 58 | } 59 | 60 | /** 61 | * Get the job with the given numeric id. 62 | * 63 | * @param int $idNum A numeric id of the job. 64 | * 65 | * @return JobInterface|null 66 | */ 67 | public function getJobFromIdNum($idNum) 68 | { 69 | return array_key_exists($idNum, $this->idToIdentifier) 70 | ? $this->jobs[$this->idToIdentifier[$idNum]] 71 | : null; 72 | } 73 | 74 | /** 75 | * Register a job for executing in batch. 76 | * 77 | * @param string $identifier Unique identifier of the job. 78 | * @param callable $callback Callback that accepts the job $idNum 79 | * and returns a JobInterface instance. 80 | * @return void 81 | */ 82 | public function registerJob($identifier, $callback) 83 | { 84 | if (array_key_exists($identifier, $this->identifierToId)) { 85 | $idNum = $this->identifierToId[$identifier]; 86 | } else { 87 | $idNum = count($this->identifierToId) + 1; 88 | $this->idToIdentifier[$idNum] = $identifier; 89 | } 90 | $this->jobs[$identifier] = call_user_func( 91 | $callback, 92 | $idNum 93 | ); 94 | $this->identifierToId[$identifier] = $idNum; 95 | } 96 | 97 | /** 98 | * Get all the jobs indexed by the job's identifier. 99 | * 100 | * @return array Associative array of JobInterface instances keyed by a 101 | * string identifier. 102 | */ 103 | public function getJobs() 104 | { 105 | return $this->jobs; 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /src/Batch/JobInterface.php: -------------------------------------------------------------------------------- 1 | identifier; 58 | } 59 | 60 | /** 61 | * Return the job id 62 | * 63 | * @return int 64 | */ 65 | public function id() 66 | { 67 | return $this->id; 68 | } 69 | 70 | /** 71 | * Returns the number of workers for this job. **Defaults to* 1. 72 | * 73 | * @return int 74 | */ 75 | public function numWorkers() 76 | { 77 | return $this->numWorkers; 78 | } 79 | 80 | /** 81 | * Returns the optional file required to run this job. 82 | * 83 | * @return string|null 84 | */ 85 | public function bootstrapFile() 86 | { 87 | return $this->bootstrapFile; 88 | } 89 | 90 | /** 91 | * Runs the job loop. This is expected to be a blocking call. 92 | */ 93 | abstract public function run(); 94 | 95 | /** 96 | * Finish any pending activity for this job. 97 | * 98 | * @param array $items 99 | * @return bool 100 | */ 101 | public function flush(array $items = []) 102 | { 103 | return false; 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /src/Batch/OpisClosureSerializer.php: -------------------------------------------------------------------------------- 1 | runner = $runner ?: new BatchRunner(); 43 | $this->initFailureFile(); 44 | } 45 | 46 | /** 47 | * Retry all the failed items. 48 | */ 49 | public function retryAll() 50 | { 51 | foreach ($this->getFailedFiles() as $file) { 52 | // Rename the file first 53 | $tmpFile = dirname($file) . '/retrying-' . basename($file); 54 | rename($file, $tmpFile); 55 | 56 | $fp = @fopen($tmpFile, 'r'); 57 | if ($fp === false) { 58 | fwrite( 59 | STDERR, 60 | sprintf('Could not open the file: %s' . PHP_EOL, $tmpFile) 61 | ); 62 | continue; 63 | } 64 | while ($line = fgets($fp)) { 65 | $jsonDecodedValue = json_decode($line); 66 | // Check if data json_encoded after serialization 67 | if ($jsonDecodedValue !== null || $jsonDecodedValue !== false) { 68 | $line = $jsonDecodedValue; 69 | } 70 | $a = unserialize($line); 71 | $idNum = key($a); 72 | $job = $this->runner->getJobFromIdNum($idNum); 73 | if (! $job->callFunc($a[$idNum])) { 74 | $this->handleFailure($idNum, $a[$idNum]); 75 | } 76 | } 77 | @fclose($fp); 78 | @unlink($tmpFile); 79 | } 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /src/Batch/SerializableClientTrait.php: -------------------------------------------------------------------------------- 1 | null, 61 | 'clientConfig' => [] 62 | ]; 63 | $this->closureSerializer = $options['closureSerializer'] 64 | ?? $this->getDefaultClosureSerializer(); 65 | $this->setWrappedClientConfig($options); 66 | } 67 | 68 | /** 69 | * @param array $options 70 | */ 71 | private function setWrappedClientConfig(array $options) 72 | { 73 | $config = $options['clientConfig'] ?? []; 74 | 75 | if ($config && $this->closureSerializer) { 76 | $this->closureSerializer->wrapClosures($config); 77 | } 78 | 79 | $this->clientConfig = $config; 80 | } 81 | 82 | /** 83 | * @return array 84 | */ 85 | private function getUnwrappedClientConfig() 86 | { 87 | if ($this->clientConfig && $this->closureSerializer) { 88 | $this->closureSerializer->unwrapClosures($this->clientConfig); 89 | } 90 | 91 | return $this->clientConfig; 92 | } 93 | 94 | /** 95 | * @return ClosureSerializerInterface|null 96 | */ 97 | private function getDefaultClosureSerializer() 98 | { 99 | if (class_exists(SerializableClosure::class)) { 100 | return new OpisClosureSerializer(); 101 | } 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /src/Batch/SimpleJob.php: -------------------------------------------------------------------------------- 1 | identifier = $identifier; 55 | $this->func = $func; 56 | $this->id = $id; 57 | 58 | $options += [ 59 | 'bootstrapFile' => null, 60 | 'numWorkers' => 1 61 | ]; 62 | $this->numWorkers = $options['numWorkers']; 63 | $this->bootstrapFile = $options['bootstrapFile']; 64 | } 65 | 66 | /** 67 | * Runs the job loop. This is expected to be a blocking call. 68 | */ 69 | public function run() 70 | { 71 | if ($this->bootstrapFile) { 72 | require_once $this->bootstrapFile; 73 | } 74 | call_user_func($this->func); 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /src/Batch/SimpleJobTrait.php: -------------------------------------------------------------------------------- 1 | null, 72 | ]; 73 | 74 | $this->setSerializableClientOptions($options); 75 | $identifier = $options['identifier']; 76 | $configStorage = $options['configStorage'] ?: $this->defaultConfigStorage(); 77 | 78 | $result = $configStorage->lock(); 79 | if ($result === false) { 80 | return false; 81 | } 82 | $config = $configStorage->load(); 83 | $config->registerJob( 84 | $identifier, 85 | function ($id) use ($identifier, $options) { 86 | return new SimpleJob($identifier, [$this, 'run'], $id, $options); 87 | } 88 | ); 89 | try { 90 | $result = $configStorage->save($config); 91 | } finally { 92 | $configStorage->unlock(); 93 | } 94 | return $result; 95 | } 96 | 97 | private function defaultConfigStorage() 98 | { 99 | if ($this->isSysvIPCLoaded() && $this->isDaemonRunning()) { 100 | return new SysvConfigStorage(); 101 | } else { 102 | return InMemoryConfigStorage::getInstance(); 103 | } 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /src/Batch/SysvProcessor.php: -------------------------------------------------------------------------------- 1 | sysvQs)) { 50 | $this->sysvQs[$idNum] = 51 | msg_get_queue($this->getSysvKey($idNum)); 52 | } 53 | $result = @msg_send( 54 | $this->sysvQs[$idNum], 55 | self::$typeDirect, 56 | $item, 57 | true, 58 | false 59 | ); 60 | if ($result === false) { 61 | // Try to put the content in a temp file and send the filename. 62 | $tempFile = tempnam(sys_get_temp_dir(), 'Item'); 63 | $result = file_put_contents($tempFile, serialize($item)); 64 | if ($result === false) { 65 | throw new \RuntimeException( 66 | "Failed to write to $tempFile while submiting the item" 67 | ); 68 | } 69 | $result = @msg_send( 70 | $this->sysvQs[$idNum], 71 | self::$typeFile, 72 | $tempFile, 73 | true, 74 | false 75 | ); 76 | if ($result === false) { 77 | @unlink($tempFile); 78 | throw new QueueOverflowException(); 79 | } 80 | } 81 | } 82 | 83 | /** 84 | * Run the job with the given id. This has no effect and simply always 85 | * returns false when using the batch daemon. 86 | * 87 | * @param int $idNum A numeric id of the job. 88 | * @return bool 89 | */ 90 | public function flush($idNum) 91 | { 92 | return false; 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /src/Blob.php: -------------------------------------------------------------------------------- 1 | value = Utils::streamFor($value); 56 | } 57 | 58 | /** 59 | * Get the blob contents as a stream 60 | * 61 | * Example: 62 | * ``` 63 | * $value = $blob->get(); 64 | * ``` 65 | * 66 | * @return StreamInterface 67 | */ 68 | public function get() 69 | { 70 | return $this->value; 71 | } 72 | 73 | /** 74 | * Cast the blob to a string 75 | * 76 | * @access private 77 | * @return string 78 | */ 79 | public function __toString() 80 | { 81 | return (string) $this->value; 82 | } 83 | 84 | /** 85 | * Implement JsonSerializable by returning a base64 encoded string of the blob 86 | * 87 | * @return string 88 | * @access private 89 | */ 90 | #[\ReturnTypeWillChange] 91 | public function jsonSerialize() 92 | { 93 | return base64_encode((string) $this->value); 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /src/CallTrait.php: -------------------------------------------------------------------------------- 1 | info()[$name])) { 31 | trigger_error(sprintf( 32 | 'Call to undefined method %s::%s', 33 | __CLASS__, 34 | $name 35 | ), E_USER_ERROR); 36 | } 37 | 38 | return $this->info()[$name]; 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/Compute/Metadata/Readers/HttpHandlerReader.php: -------------------------------------------------------------------------------- 1 | httpHandler = $httpHandler 42 | ?: HttpHandlerFactory::build(HttpClientCache::getHttpClient()); 43 | } 44 | 45 | /** 46 | * Read the metadata for a given path. 47 | * 48 | * @param string $path The metadata path, relative to `/computeMetadata/v1/`. 49 | * @return string 50 | */ 51 | public function read($path) 52 | { 53 | $url = sprintf( 54 | 'http://%s/computeMetadata/v1/%s', 55 | GCECredentials::METADATA_IP, 56 | $path 57 | ); 58 | 59 | $request = new Request('GET', $url, [ 60 | GCECredentials::FLAVOR_HEADER => 'Google' 61 | ]); 62 | 63 | $handler = $this->httpHandler; 64 | $res = $handler($request); 65 | 66 | return (string) $res->getBody(); 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /src/Compute/Metadata/Readers/ReaderInterface.php: -------------------------------------------------------------------------------- 1 | [ 54 | 'method' => 'GET', 55 | 'header' => GCECredentials::FLAVOR_HEADER . ': Google' 56 | ], 57 | ]; 58 | $this->context = $this->createStreamContext($options); 59 | } 60 | 61 | /** 62 | * Read the metadata for a given path. 63 | * 64 | * @param string $path The metadata path, relative to `/computeMetadata/v1/`. 65 | * @return string 66 | */ 67 | public function read($path) 68 | { 69 | $url = sprintf( 70 | 'http://%s/computeMetadata/v1/%s', 71 | GCECredentials::METADATA_IP, 72 | $path 73 | ); 74 | 75 | return $this->getMetadata($url); 76 | } 77 | 78 | /** 79 | * Abstracted for testing. 80 | * 81 | * @param array $options 82 | * @return resource 83 | * @codeCoverageIgnore 84 | */ 85 | protected function createStreamContext(array $options) 86 | { 87 | return stream_context_create($options); 88 | } 89 | 90 | /** 91 | * Abstracted for testing. 92 | * 93 | * @param string $url 94 | * @return string 95 | * @codeCoverageIgnore 96 | */ 97 | protected function getMetadata($url) 98 | { 99 | return file_get_contents($url, false, $this->context); 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /src/ConcurrencyControlTrait.php: -------------------------------------------------------------------------------- 1 | connection)) { 36 | $props['connection'] = get_class($this->connection); 37 | } 38 | 39 | if (isset($props['__excludeFromDebug'])) { 40 | $exclude = $props['__excludeFromDebug']; 41 | unset($props['__excludeFromDebug']); 42 | 43 | foreach ($exclude as $e) { 44 | unset($props[$e]); 45 | } 46 | } 47 | 48 | return $props; 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/DetectProjectIdTrait.php: -------------------------------------------------------------------------------- 1 | null, 57 | 'projectId' => null, 58 | 'projectIdRequired' => false, 59 | 'hasEmulator' => false, 60 | 'credentials' => null, 61 | ]; 62 | 63 | if ($config['projectId']) { 64 | return $config['projectId']; 65 | } 66 | 67 | if ($config['hasEmulator']) { 68 | return 'emulator-project'; 69 | } 70 | 71 | if ($config['credentials'] 72 | && $config['credentials'] instanceof ProjectIdProviderInterface 73 | && $projectId = $config['credentials']->getProjectId()) { 74 | return $projectId; 75 | } 76 | 77 | if (getenv('GOOGLE_CLOUD_PROJECT')) { 78 | return getenv('GOOGLE_CLOUD_PROJECT'); 79 | } 80 | 81 | if (getenv('GCLOUD_PROJECT')) { 82 | return getenv('GCLOUD_PROJECT'); 83 | } 84 | 85 | $this->throwExceptionIfProjectIdRequired($config); 86 | 87 | return ''; 88 | } 89 | 90 | /** 91 | * Throws an exception if project id is required. 92 | * @param array $config 93 | * @throws GoogleException 94 | */ 95 | private function throwExceptionIfProjectIdRequired(array $config) 96 | { 97 | if ($config['projectIdRequired']) { 98 | throw new GoogleException( 99 | 'No project ID was provided, ' . 100 | 'and we were unable to detect a default project ID.' 101 | ); 102 | } 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /src/Duration.php: -------------------------------------------------------------------------------- 1 | seconds = $seconds; 58 | $this->nanos = $nanos; 59 | } 60 | 61 | /** 62 | * Get the duration 63 | * 64 | * Example: 65 | * ``` 66 | * $res = $duration->get(); 67 | * ``` 68 | * 69 | * @return array 70 | */ 71 | public function get() 72 | { 73 | return [ 74 | 'seconds' => $this->seconds, 75 | 'nanos' => $this->nanos 76 | ]; 77 | } 78 | 79 | /** 80 | * Format the value as a string. 81 | * 82 | * Example: 83 | * ``` 84 | * echo $duration->formatAsString(); 85 | * ``` 86 | * 87 | * @return string 88 | */ 89 | public function formatAsString() 90 | { 91 | return json_encode($this->get()); 92 | } 93 | 94 | /** 95 | * Format the value as a string. 96 | * 97 | * @return string 98 | * @access private 99 | */ 100 | public function __toString() 101 | { 102 | return $this->formatAsString(); 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /src/EmulatorTrait.php: -------------------------------------------------------------------------------- 1 | $emulatorHost, 41 | 'credentials' => new InsecureCredentialsWrapper(), 42 | ]; 43 | if (class_exists('Grpc\ChannelCredentials')) { 44 | $config['transportConfig'] = [ 45 | 'grpc' => [ 46 | 'stubOpts' => [ 47 | 'credentials' => \Grpc\ChannelCredentials::createInsecure() 48 | ] 49 | ] 50 | ]; 51 | } 52 | 53 | return $config; 54 | } 55 | /** 56 | * Retrieve a valid base uri for a service emulator. 57 | * 58 | * @param string $emulatorHost 59 | * @return string 60 | */ 61 | private function emulatorBaseUri($emulatorHost) 62 | { 63 | $emulatorUriComponents = parse_url($emulatorHost); 64 | $emulatorUriComponents = array_merge(['scheme' => 'http', 'port' => ''], $emulatorUriComponents); 65 | $baseUri = "{$emulatorUriComponents['scheme']}://{$emulatorUriComponents['host']}"; 66 | $baseUri .= $emulatorUriComponents['port'] ? ":{$emulatorUriComponents['port']}/" : '/'; 67 | 68 | return $baseUri; 69 | } 70 | 71 | /** 72 | * When emulators are enabled, use them as the service host. 73 | * 74 | * This method is deprecated and will be removed in a future major release. 75 | * 76 | * @param string $baseUri 77 | * @param string $emulatorHost [optional] 78 | * @return string 79 | * 80 | * @deprecated 81 | * @access private 82 | */ 83 | public function getEmulatorBaseUri($baseUri, $emulatorHost = null) 84 | { 85 | if ($emulatorHost) { 86 | $baseUri = $this->emulatorBaseUri($emulatorHost); 87 | } 88 | 89 | return $baseUri; 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /src/Exception/AbortedException.php: -------------------------------------------------------------------------------- 1 | message = $message; 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/Exception/ServerException.php: -------------------------------------------------------------------------------- 1 | value = $value; 42 | } 43 | 44 | /** 45 | * Get the value. 46 | * 47 | * Example: 48 | * ``` 49 | * $value = $int64->get(); 50 | * ``` 51 | * 52 | * @return string 53 | */ 54 | public function get() 55 | { 56 | return $this->value; 57 | } 58 | 59 | /** 60 | * Provides a convenient way to access the value. 61 | * 62 | * @access private 63 | */ 64 | public function __toString() 65 | { 66 | return $this->value; 67 | } 68 | 69 | /** 70 | * Implement JsonSerializable by returning the 64 bit integer as a string 71 | * 72 | * @return string 73 | * @access private 74 | */ 75 | #[\ReturnTypeWillChange] 76 | public function jsonSerialize() 77 | { 78 | return $this->value; 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /src/Iterator/ItemIterator.php: -------------------------------------------------------------------------------- 1 | 25 | */ 26 | class ItemIterator implements \Iterator 27 | { 28 | use ItemIteratorTrait; 29 | } 30 | -------------------------------------------------------------------------------- /src/Iterator/PageIterator.php: -------------------------------------------------------------------------------- 1 | acquire($options)) { 66 | try { 67 | $result = $func(); 68 | } catch (\Exception $ex) { 69 | $exception = $ex; 70 | } 71 | $this->release(); 72 | } 73 | 74 | if ($exception) { 75 | throw $exception; 76 | } 77 | 78 | return $result; 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /src/Lock/SemaphoreLock.php: -------------------------------------------------------------------------------- 1 | isSysvIPCLoaded()) { 54 | throw new \RuntimeException('SystemV IPC extensions are required.'); 55 | } 56 | 57 | if (!is_int($key)) { 58 | throw new \InvalidArgumentException('The provided key must be an integer.'); 59 | } 60 | 61 | $this->key = $key; 62 | } 63 | 64 | /** 65 | * Acquires a lock that will block until released. 66 | * 67 | * @param array $options [optional] { 68 | * Configuration options. 69 | * 70 | * @type bool $blocking Whether the process should block while waiting 71 | * to acquire the lock. **Defaults to** true. 72 | * } 73 | * @return bool 74 | * @throws \RuntimeException If the lock fails to be acquired. 75 | */ 76 | public function acquire(array $options = []) 77 | { 78 | $options += [ 79 | 'blocking' => true 80 | ]; 81 | 82 | if ($this->semaphoreId) { 83 | return true; 84 | } 85 | 86 | $this->semaphoreId = $this->initializeId(); 87 | 88 | if (!sem_acquire($this->semaphoreId, !$options['blocking'])) { 89 | $this->semaphoreId = null; 90 | 91 | throw new \RuntimeException('Failed to acquire lock.'); 92 | } 93 | 94 | return true; 95 | } 96 | 97 | /** 98 | * Releases the lock. 99 | * 100 | * @throws \RuntimeException If the lock fails to release. 101 | */ 102 | public function release() 103 | { 104 | if ($this->semaphoreId) { 105 | $released = sem_release($this->semaphoreId); 106 | $this->semaphoreId = null; 107 | 108 | if (!$released) { 109 | throw new \RuntimeException('Failed to release lock.'); 110 | } 111 | } 112 | } 113 | 114 | /** 115 | * Initializes the semaphore ID. 116 | * 117 | * @return resource 118 | * @throws \RuntimeException If semaphore ID fails to generate. 119 | */ 120 | private function initializeId() 121 | { 122 | $semaphoreId = sem_get($this->key); 123 | 124 | if (!$semaphoreId) { 125 | throw new \RuntimeException('Failed to generate semaphore ID.'); 126 | } 127 | 128 | return $semaphoreId; 129 | } 130 | } 131 | -------------------------------------------------------------------------------- /src/Lock/SymfonyLockAdapter.php: -------------------------------------------------------------------------------- 1 | lock = $lock; 41 | } 42 | 43 | /** 44 | * Acquires a lock that will block until released. 45 | * 46 | * @param array $options [optional] { 47 | * Configuration options. 48 | * 49 | * @type bool $blocking Whether the process should block while waiting 50 | * to acquire the lock. **Defaults to** true. 51 | * } 52 | * @return bool 53 | * @throws \RuntimeException If the lock fails to be acquired. 54 | */ 55 | public function acquire(array $options = []) 56 | { 57 | $options += [ 58 | 'blocking' => true 59 | ]; 60 | 61 | try { 62 | return $this->lock->acquire($options['blocking']); 63 | } catch (\Exception $ex) { 64 | throw new \RuntimeException( 65 | sprintf( 66 | 'Acquiring the lock failed with the following message: %s', 67 | $ex->getMessage() 68 | ), 69 | 0, 70 | $ex 71 | ); 72 | } 73 | } 74 | 75 | /** 76 | * Releases the lock. 77 | * 78 | * @throws \RuntimeException 79 | */ 80 | public function release() 81 | { 82 | try { 83 | $this->lock->release(); 84 | } catch (\Exception $ex) { 85 | throw new \RuntimeException( 86 | sprintf( 87 | 'Releasing the lock failed with the following message: %s', 88 | $ex->getMessage() 89 | ), 90 | 0, 91 | $ex 92 | ); 93 | } 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /src/Logger/AppEngineFlexFormatter.php: -------------------------------------------------------------------------------- 1 | formatPayload($record, parent::format($record)); 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/Logger/AppEngineFlexFormatterV2.php: -------------------------------------------------------------------------------- 1 | formatPayload($record, parent::format($record)); 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/Logger/AppEngineFlexFormatterV3.php: -------------------------------------------------------------------------------- 1 | formatPayload($record, parent::format($record)); 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/Logger/AppEngineFlexHandler.php: -------------------------------------------------------------------------------- 1 | toArray(); 36 | } 37 | 38 | list($usec, $sec) = explode(' ', microtime()); 39 | $usec = (int) (((float) $usec) * 1000000000); 40 | $sec = (int) $sec; 41 | 42 | $payload = [ 43 | 'message' => $message, 44 | 'timestamp' => ['seconds' => $sec, 'nanos' => $usec], 45 | 'thread' => '', 46 | 'severity' => $record['level_name'], 47 | ]; 48 | 49 | if (isset($_SERVER['HTTP_X_CLOUD_TRACE_CONTEXT'])) { 50 | $payload['traceId'] = explode( 51 | '/', 52 | $_SERVER['HTTP_X_CLOUD_TRACE_CONTEXT'] 53 | )[0]; 54 | } 55 | 56 | return "\n" . json_encode($payload); 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /src/LongRunning/LongRunningConnectionInterface.php: -------------------------------------------------------------------------------- 1 | serviceId = $env['CLOUD_RUN_JOB'] ?? 'unknown-job'; 74 | $this->revisionId = $env['CLOUD_RUN_EXECUTION'] ?? ''; 75 | $this->taskIndex = $env['CLOUD_RUN_TASK_INDEX'] ?? ''; 76 | $this->taskAttempt = $env['CLOUD_RUN_TASK_ATTEMPT'] ?? ''; 77 | 78 | $this->traceId = isset($env['HTTP_X_CLOUD_TRACE_CONTEXT']) 79 | ? \substr($env['HTTP_X_CLOUD_TRACE_CONTEXT'], 0, 32) 80 | : null; 81 | 82 | $this->metadata = $metadata ?? new Metadata(); 83 | $this->region = \basename($this->metadata->get('instance/region')); 84 | $this->instanceId = $this->metadata->get('instance/id'); 85 | } 86 | 87 | /** 88 | * @return array 89 | */ 90 | public function monitoredResource() 91 | { 92 | return [ 93 | 'type' => 'cloud_run_job', 94 | 'labels' => [ 95 | 'job_name' => $this->serviceId, 96 | 'location' => $this->region, 97 | 'project_id' => $this->projectId(), 98 | ], 99 | ]; 100 | } 101 | 102 | /** 103 | * Return the project id. 104 | * @return string 105 | */ 106 | public function projectId() 107 | { 108 | return $this->metadata->getProjectId(); 109 | } 110 | 111 | /** 112 | * Return the service id. 113 | * @return string 114 | */ 115 | public function serviceId() 116 | { 117 | return $this->serviceId; 118 | } 119 | 120 | /** 121 | * Return the version id. 122 | * @return string 123 | */ 124 | public function versionId() 125 | { 126 | return $this->revisionId; 127 | } 128 | 129 | /** 130 | * Return the labels. 131 | * @return array 132 | */ 133 | public function labels() 134 | { 135 | $labels = [ 136 | 'instanceId' => $this->instanceId, 137 | 'run.googleapis.com/execution_name' => $this->revisionId, 138 | 'run.googleapis.com/task_attempt' => $this->taskAttempt, 139 | 'run.googleapis.com/task_index' => $this->taskIndex, 140 | 'run.googleapis.com/trace_id' => $this->traceId, 141 | ]; 142 | return \array_filter($labels); 143 | } 144 | } 145 | -------------------------------------------------------------------------------- /src/Report/CloudRunMetadataProvider.php: -------------------------------------------------------------------------------- 1 | configurationId = $env['K_CONFIGURATION'] ?? 'unknown-configuration'; 69 | $this->serviceId = $env['K_SERVICE'] ?? 'unknown-service'; 70 | $this->revisionId = $env['K_REVISION'] ?? 'unknown-revision'; 71 | $this->traceId = isset($env['HTTP_X_CLOUD_TRACE_CONTEXT']) 72 | ? \substr($env['HTTP_X_CLOUD_TRACE_CONTEXT'], 0, 32) 73 | : null; 74 | 75 | $this->metadata = $metadata ?? new Metadata(); 76 | $this->region = \basename($this->metadata->get('instance/region')); 77 | $this->instanceId = $this->metadata->get('instance/id'); 78 | } 79 | 80 | /** 81 | * @return array 82 | */ 83 | public function monitoredResource() 84 | { 85 | return [ 86 | 'type' => 'cloud_run_revision', 87 | 'labels' => [ 88 | 'configuration_name' => $this->configurationId, 89 | 'location' => $this->region, 90 | 'project_id' => $this->projectId(), 91 | 'revision_name' => $this->revisionId, 92 | 'service_name' => $this->serviceId, 93 | ], 94 | ]; 95 | } 96 | 97 | /** 98 | * Return the project id. 99 | * @return string 100 | */ 101 | public function projectId() 102 | { 103 | return $this->metadata->getProjectId(); 104 | } 105 | 106 | /** 107 | * Return the service id. 108 | * @return string 109 | */ 110 | public function serviceId() 111 | { 112 | return $this->serviceId; 113 | } 114 | 115 | /** 116 | * Return the version id. 117 | * @return string 118 | */ 119 | public function versionId() 120 | { 121 | return $this->revisionId; 122 | } 123 | 124 | /** 125 | * Return the labels. We need to evaluate $_SERVER for each request. 126 | * 127 | * @return array 128 | * @todo Figure out where to get the `container_name` from 129 | */ 130 | public function labels() 131 | { 132 | $labels = [ 133 | 'instanceId' => $this->instanceId, 134 | 'run.googleapis.com/trace_id' => $this->traceId, 135 | ]; 136 | return \array_filter($labels); 137 | } 138 | } 139 | -------------------------------------------------------------------------------- /src/Report/EmptyMetadataProvider.php: -------------------------------------------------------------------------------- 1 | 41 | $this->getTraceValue($server)] 42 | : []; 43 | $this->data = 44 | [ 45 | 'resource' => [ 46 | 'type' => 'gae_app', 47 | 'labels' => [ 48 | 'project_id' => $projectId, 49 | 'version_id' => $versionId, 50 | 'module_id' => $serviceId 51 | ] 52 | ], 53 | 'projectId' => $projectId, 54 | 'serviceId' => $serviceId, 55 | 'versionId' => $versionId, 56 | 'labels' => $labels 57 | ]; 58 | } 59 | 60 | /** 61 | * Return an array representing MonitoredResource. 62 | * 63 | * @see https://cloud.google.com/logging/docs/reference/v2/rest/v2/MonitoredResource 64 | * 65 | * @return array 66 | */ 67 | public function monitoredResource() 68 | { 69 | return $this->data['resource']; 70 | } 71 | 72 | /** 73 | * Return the project id. 74 | * @return string 75 | */ 76 | public function projectId() 77 | { 78 | return $this->data['projectId']; 79 | } 80 | 81 | /** 82 | * Return the service id. 83 | * @return string 84 | */ 85 | public function serviceId() 86 | { 87 | return $this->data['serviceId']; 88 | } 89 | 90 | /** 91 | * Return the version id. 92 | * @return string 93 | */ 94 | public function versionId() 95 | { 96 | return $this->data['versionId']; 97 | } 98 | 99 | /** 100 | * Return the labels. We need to evaluate $_SERVER for each request. 101 | * @return array 102 | */ 103 | public function labels() 104 | { 105 | return $this->data['labels']; 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /src/Report/GAEStandardMetadataProvider.php: -------------------------------------------------------------------------------- 1 | data['monitoredResource'] = $monitoredResource; 44 | $this->data['projectId'] = $projectId; 45 | $this->data['serviceId'] = $serviceId; 46 | $this->data['versionId'] = $versionId; 47 | $this->data['labels'] = $labels; 48 | } 49 | 50 | /** 51 | * Return an array representing MonitoredResource. 52 | * 53 | * @see https://cloud.google.com/logging/docs/reference/v2/rest/v2/MonitoredResource 54 | * 55 | * @return array 56 | */ 57 | public function monitoredResource() 58 | { 59 | return $this->data['monitoredResource']; 60 | } 61 | 62 | /** 63 | * Return the project id. 64 | * @return string 65 | */ 66 | public function projectId() 67 | { 68 | return $this->data['projectId']; 69 | } 70 | 71 | /** 72 | * Return the service id. 73 | * @return string 74 | */ 75 | public function serviceId() 76 | { 77 | return $this->data['serviceId']; 78 | } 79 | 80 | /** 81 | * Return the version id. 82 | * @return string 83 | */ 84 | public function versionId() 85 | { 86 | return $this->data['versionId']; 87 | } 88 | 89 | /** 90 | * Return the labels. 91 | * @return array 92 | */ 93 | public function labels() 94 | { 95 | return $this->data['labels']; 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /src/Retry.php: -------------------------------------------------------------------------------- 1 | (int >= 0), 'nanos' => (int >= 0)] specifying how 49 | * long an operation should pause before retrying. Should accept a 50 | * single argument of type `\Exception`. 51 | * @param callable $retryFunction [optional] returns bool for whether or not 52 | * to retry. 53 | */ 54 | public function __construct( 55 | $retries, 56 | callable $delayFunction, 57 | ?callable $retryFunction = null 58 | ) { 59 | $this->retries = $retries !== null ? (int) $retries : 3; 60 | $this->delayFunction = $delayFunction; 61 | $this->retryFunction = $retryFunction; 62 | } 63 | 64 | /** 65 | * Executes the retry process. 66 | * 67 | * @param callable $function 68 | * @param array $arguments [optional] 69 | * @return mixed 70 | * @throws \Exception The last exception caught while retrying. 71 | */ 72 | public function execute(callable $function, array $arguments = []) 73 | { 74 | $delayFunction = $this->delayFunction; 75 | $retryAttempt = 0; 76 | 77 | $continue = true; 78 | do { 79 | try { 80 | $res = call_user_func_array($function, $arguments); 81 | $continue = false; 82 | return $res; 83 | } catch (\Exception $exception) { 84 | if ($this->retryFunction) { 85 | if (!call_user_func($this->retryFunction, $exception, $retryAttempt)) { 86 | throw $exception; 87 | } 88 | } 89 | 90 | if ($retryAttempt < $this->retries) { 91 | $delay = $delayFunction($exception); 92 | $delay += [ 93 | 'seconds' => 0, 94 | 'nanos' => 0 95 | ]; 96 | 97 | time_nanosleep($delay['seconds'], $delay['nanos']); 98 | 99 | $retryAttempt++; 100 | } else { 101 | $continue = false; 102 | throw $exception; 103 | } 104 | } 105 | } while ($continue); 106 | } 107 | 108 | /** 109 | * @param callable $delayFunction 110 | * @return void 111 | */ 112 | public function setDelayFunction(callable $delayFunction) 113 | { 114 | $this->delayFunction = $delayFunction; 115 | } 116 | } 117 | -------------------------------------------------------------------------------- /src/RetryDeciderTrait.php: -------------------------------------------------------------------------------- 1 | httpRetryCodes; 56 | $httpRetryMessages = $this->httpRetryMessages; 57 | 58 | return function (\Exception $ex) use ($httpRetryCodes, $httpRetryMessages, $shouldRetryMessages) { 59 | $statusCode = $ex->getCode(); 60 | 61 | if (in_array($statusCode, $httpRetryCodes)) { 62 | return true; 63 | } 64 | 65 | if (!$shouldRetryMessages) { 66 | return false; 67 | } 68 | 69 | $message = ($ex instanceof RequestException && $ex->hasResponse()) 70 | ? (string) $ex->getResponse()->getBody() 71 | : $ex->getMessage(); 72 | 73 | try { 74 | $message = $this->jsonDecode( 75 | $message, 76 | true 77 | ); 78 | } catch (\InvalidArgumentException $ex) { 79 | return false; 80 | } 81 | 82 | if (!isset($message['error']['errors'])) { 83 | return false; 84 | } 85 | 86 | foreach ($message['error']['errors'] as $error) { 87 | if (in_array($error['reason'], $httpRetryMessages)) { 88 | return true; 89 | } 90 | } 91 | 92 | return false; 93 | }; 94 | } 95 | 96 | /** 97 | * @param array $codes 98 | */ 99 | private function setHttpRetryCodes(array $codes) 100 | { 101 | $this->httpRetryCodes = $codes; 102 | } 103 | 104 | /** 105 | * @param array $messages 106 | */ 107 | private function setHttpRetryMessages(array $messages) 108 | { 109 | $this->httpRetryMessages = $messages; 110 | } 111 | } 112 | -------------------------------------------------------------------------------- /src/SysvTrait.php: -------------------------------------------------------------------------------- 1 | value = $value; 31 | $this->util = $util ?: new StringUtil(); 32 | } 33 | 34 | /** 35 | * @param mixed $argument 36 | * @return bool|int 37 | * 38 | * @experimental 39 | * @internal 40 | */ 41 | public function scoreArgument($argument) 42 | { 43 | return $this->compare($this->value, $argument) ? 11 : false; 44 | } 45 | 46 | private function compare(array $value, array $argument) 47 | { 48 | array_multisort($value); 49 | array_multisort($argument); 50 | 51 | return $value == $argument; 52 | } 53 | 54 | /** 55 | * @return bool 56 | * 57 | * @experimental 58 | * @internal 59 | */ 60 | public function isLast() 61 | { 62 | return false; 63 | } 64 | 65 | /** 66 | * @return string 67 | * 68 | * @experimental 69 | * @internal 70 | */ 71 | public function __toString() 72 | { 73 | if ($this->string) { 74 | $string = $this->string .': (%s)'; 75 | } else { 76 | $string = 'same(%s)'; 77 | } 78 | 79 | return sprintf($string, $this->util->stringify($this->value)); 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /src/Testing/CheckForClassTrait.php: -------------------------------------------------------------------------------- 1 | markTestSkipped("Missing required class: $class"); 41 | return; 42 | } 43 | } 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/Testing/DatastoreOperationRefreshTrait.php: -------------------------------------------------------------------------------- 1 | null, 50 | 'returnInt64AsObject' => false, 51 | 'encode' => false 52 | ]; 53 | 54 | $mapper = new EntityMapper( 55 | $options['projectId'], 56 | $options['encode'], 57 | $options['returnInt64AsObject'] 58 | ); 59 | 60 | $stub->___setProperty('operation', new Operation( 61 | $connection, 62 | $options['projectId'], 63 | $options['returnInt64AsObject'], 64 | $mapper 65 | )); 66 | 67 | return $stub; 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /src/Testing/FileListFilterIterator.php: -------------------------------------------------------------------------------- 1 | projectRootPath = $projectRootPath; 50 | $this->fileTypes = $fileTypes; 51 | $this->testPaths = $testPaths; 52 | $this->excludes = $excludes; 53 | 54 | parent::__construct($iterator); 55 | } 56 | 57 | /** 58 | * Decides whether to include the file or exclude it. 59 | * 60 | * @return bool 61 | * 62 | * @experimental 63 | * @internal 64 | */ 65 | #[\ReturnTypeWillChange] 66 | public function accept() 67 | { 68 | /** @var \SplFileInfo */ 69 | $file = parent::current(); 70 | 71 | $path = '/' . trim(str_replace($this->projectRootPath, '', realpath($file->getPathName())), '/'); 72 | 73 | if (!in_array($file->getExtension(), $this->fileTypes)) { 74 | return false; 75 | } 76 | 77 | foreach ($this->excludes as $exclude) { 78 | if ($exclude instanceof RegexFileFilter) { 79 | $pattern = $exclude->regex; 80 | 81 | if (preg_match($pattern, $path) === 1) { 82 | return false; 83 | } 84 | 85 | continue; 86 | } 87 | 88 | if (strpos($exclude, '/') !== 0 && strpos($path, $exclude) !== false) { 89 | return false; 90 | } 91 | 92 | if (strpos($path, $exclude) === 0) { 93 | return false; 94 | } 95 | } 96 | 97 | foreach ($this->testPaths as $testPath) { 98 | if (strpos($path, $testPath) !== false) { 99 | return false; 100 | } 101 | } 102 | 103 | return true; 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /src/Testing/GcTestListener.php: -------------------------------------------------------------------------------- 1 | markTestSkipped('Must have the grpc extension installed to run this test.'); 38 | } 39 | if (defined('HHVM_VERSION')) { 40 | $this->markTestSkipped('gRPC is not supported on HHVM.'); 41 | } 42 | } 43 | 44 | /** 45 | * @return bool True if grpc tests should be skipped, otherwise false 46 | * 47 | * @experimental 48 | * @internal 49 | */ 50 | public function shouldSkipGrpcTests() 51 | { 52 | return !extension_loaded('grpc') || defined('HHVM_VERSION'); 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/Testing/KeyPairGenerateTrait.php: -------------------------------------------------------------------------------- 1 | withPadding(RSA3::SIGNATURE_PKCS1) 39 | ->withHash('sha256'); 40 | 41 | return [$key->toString('PKCS1'), $key->getPublicKey()]; 42 | } 43 | 44 | $rsa = new RSA2; 45 | $rsa->setSignatureMode(RSA2::SIGNATURE_PKCS1); 46 | $rsa->setHash('sha256'); 47 | 48 | $key = $rsa->createKey(); 49 | usleep(500); 50 | return [$key['privatekey'], $key['publickey']]; 51 | } 52 | 53 | private function verifySignature($privateKey, $input, $signature) 54 | { 55 | $verify = $this->signString($privateKey, $input); 56 | 57 | return urlencode(base64_encode($verify)) === $signature; 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /src/Testing/Lock/MockGlobals.php: -------------------------------------------------------------------------------- 1 | createForVersion($phpVersion); 37 | } 38 | 39 | /** 40 | * @see ReflectionHandlerV5 41 | */ 42 | protected function getAdditionalStrategies() 43 | { 44 | return []; 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/Testing/RegexFileFilter.php: -------------------------------------------------------------------------------- 1 | regex = $regex; 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/Testing/Snippet/Container.php: -------------------------------------------------------------------------------- 1 | scanner = $scanner; 63 | } 64 | 65 | private function getSnippetExcludeList() 66 | { 67 | return static::$snippetExcludeList; 68 | } 69 | 70 | /** 71 | * Creates a list of all snippets which should be covered. 72 | * 73 | * @return Snippet[] 74 | * 75 | * @experimental 76 | * @internal 77 | */ 78 | public function buildListToCover() 79 | { 80 | $files = $this->scanner->files(); 81 | $classes = $this->scanner->classes($files, $this->getSnippetExcludeList()); 82 | 83 | $this->snippets = $this->scanner->snippets($classes); 84 | 85 | return $this->snippets; 86 | } 87 | 88 | /** 89 | * Mark a snippet as covered. 90 | * 91 | * @param string $identifier The identifier of the snippet being covered. 92 | * @return void 93 | * 94 | * @experimental 95 | * @internal 96 | */ 97 | public function cover($identifier) 98 | { 99 | $this->covered[] = $identifier; 100 | } 101 | 102 | /** 103 | * Return a list of all snippets not marked a covered. 104 | * 105 | * @return Snippet[] 106 | * 107 | * @experimental 108 | * @internal 109 | */ 110 | public function uncovered() 111 | { 112 | return array_diff_key($this->snippets, array_flip($this->covered)); 113 | } 114 | 115 | /** 116 | * @param string|int $identifier 117 | * @return Snippet|null 118 | * 119 | * @experimental 120 | * @internal 121 | */ 122 | public function cache($identifier) 123 | { 124 | return (array_key_exists($identifier, $this->snippets)) 125 | ? $this->snippets[$identifier] 126 | : null; 127 | } 128 | } 129 | -------------------------------------------------------------------------------- /src/Testing/Snippet/Coverage/ExcludeFilter.php: -------------------------------------------------------------------------------- 1 | excludeDirs = $excludeDirs; 42 | } 43 | 44 | /** 45 | * @return bool Determines whether to accept or exclude a path 46 | */ 47 | #[\ReturnTypeWillChange] 48 | public function accept() 49 | { 50 | // Accept the current item if we can recurse into it 51 | // or it is a value starting with "test" 52 | foreach ($this->excludeDirs as $excludeDir) { 53 | if (strpos($this->current(), $excludeDir) !== false) { 54 | return false; 55 | } 56 | } 57 | return true; 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /src/Testing/Snippet/Coverage/ScannerInterface.php: -------------------------------------------------------------------------------- 1 | returnVal = $returnVal; 37 | $this->output = $output; 38 | } 39 | 40 | /** 41 | * @return mixed 42 | */ 43 | public function returnVal() 44 | { 45 | return $this->returnVal; 46 | } 47 | 48 | /** 49 | * @return mixed 50 | */ 51 | public function output() 52 | { 53 | return $this->output; 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/Testing/Snippet/keyfile-stub.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "service_account", 3 | "project_id": "my-awesome-project", 4 | "private_key_id": "", 5 | "private_key": "", 6 | "client_email": "", 7 | "client_id": "", 8 | "auth_uri": "", 9 | "token_uri": "", 10 | "auth_provider_x509_cert_url": "", 11 | "client_x509_cert_url": "" 12 | } 13 | -------------------------------------------------------------------------------- /src/Testing/StubTrait.php: -------------------------------------------------------------------------------- 1 | ___getPropertyReflector($prop); 38 | 39 | $property->setAccessible(true); 40 | return $property->getValue($this); 41 | } 42 | 43 | /** 44 | * @param $prop 45 | * @param $value 46 | * 47 | * @experimental 48 | * @internal 49 | */ 50 | public function ___setProperty($prop, $value) 51 | { 52 | if (!in_array($prop, json_decode($this->___props))) { 53 | throw new \RuntimeException(sprintf('Property %s cannot be overloaded', $prop)); 54 | } 55 | 56 | $property = $this->___getPropertyReflector($prop); 57 | 58 | $property->setAccessible(true); 59 | $property->setValue($this, $value); 60 | } 61 | 62 | private function ___getPropertyReflector($property) 63 | { 64 | $trait = new \ReflectionClass($this); 65 | $ref = $trait->getParentClass() ?: $trait; 66 | 67 | // wrap this in a loop that will iterate up a class hierarchy to try 68 | // and find a private property. 69 | $keepTrying = true; 70 | do { 71 | try { 72 | $property = $ref->getProperty($property); 73 | $keepTrying = false; 74 | } catch (\ReflectionException $e) { 75 | if ($ref->getParentClass()) { 76 | $ref = $ref->getParentClass(); 77 | } else { 78 | throw new \BadMethodCallException($e->getMessage()); 79 | } 80 | } 81 | } while ($keepTrying); 82 | 83 | return $property; 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /src/Testing/System/DeletionQueue.php: -------------------------------------------------------------------------------- 1 | acceptAllInputs = $acceptAllInputs; 52 | } 53 | 54 | /** 55 | * Add an item to be cleaned up. 56 | * 57 | * @param mixed $toDelete Unless the class was created with 58 | * `$acceptAllInputs = true`, either a callable with no arguments, or 59 | * an object with a `delete` method. 60 | * @return void 61 | * 62 | * @experimental 63 | * @internal 64 | */ 65 | public function add($toDelete) 66 | { 67 | if (!$this->acceptAllInputs) { 68 | if (!is_callable($toDelete) && !method_exists($toDelete, 'delete')) { 69 | throw new \BadMethodCallException( 70 | 'Deletion Queue requires a callable, or an object with a `delete` method.' 71 | ); 72 | } 73 | 74 | if (!is_callable($toDelete)) { 75 | $toDelete = function () use ($toDelete) { 76 | $toDelete->delete(); 77 | }; 78 | } 79 | } 80 | 81 | $this->queue[] = $toDelete; 82 | } 83 | 84 | /** 85 | * Process all items in the deletion queue. 86 | * 87 | * @return void 88 | * @throws ApiException 89 | * 90 | * @experimental 91 | * @internal 92 | */ 93 | public function process(?callable $action = null) 94 | { 95 | if ($action) { 96 | $action($this->queue); 97 | } else { 98 | $backoff = new ExponentialBackoff(8); 99 | 100 | foreach ($this->queue as $item) { 101 | $backoff->execute(function () use ($item) { 102 | try { 103 | call_user_func($item); 104 | } catch (NotFoundException $e) { 105 | } catch (ApiException $apiException) { 106 | if ($apiException->getStatus() !== 'NOT_FOUND') { 107 | throw $apiException; 108 | } 109 | } 110 | }); 111 | } 112 | } 113 | } 114 | } 115 | -------------------------------------------------------------------------------- /src/TimestampTrait.php: -------------------------------------------------------------------------------- 1 | formatForApi(); 47 | } 48 | return $args; 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/Upload/AbstractUploader.php: -------------------------------------------------------------------------------- 1 | requestWrapper = $requestWrapper; 99 | $this->data = Utils::streamFor($data); 100 | $this->uri = $uri; 101 | $this->metadata = $options['metadata'] ?? []; 102 | $this->chunkSize = $options['chunkSize'] ?? null; 103 | $this->requestOptions = array_intersect_key($options, [ 104 | 'restOptions' => null, 105 | 'retries' => null, 106 | 'requestTimeout' => null, 107 | 'restRetryFunction' => null, 108 | 'restRetryListener' => null 109 | ]); 110 | 111 | $this->contentType = $options['contentType'] ?? 'application/octet-stream'; 112 | } 113 | 114 | /** 115 | * @return array 116 | */ 117 | abstract public function upload(); 118 | } 119 | -------------------------------------------------------------------------------- /src/Upload/MultipartUploader.php: -------------------------------------------------------------------------------- 1 | jsonDecode( 42 | $this->requestWrapper->send( 43 | $this->prepareRequest(), 44 | $this->requestOptions 45 | )->getBody(), 46 | true 47 | ); 48 | } 49 | 50 | /** 51 | * Triggers the upload process asynchronously. 52 | * 53 | * @return PromiseInterface 54 | * @experimental The experimental flag means that while we believe this method 55 | * or class is ready for use, it may change before release in backwards- 56 | * incompatible ways. Please use with caution, and test thoroughly when 57 | * upgrading. 58 | */ 59 | public function uploadAsync() 60 | { 61 | return $this->requestWrapper->sendAsync( 62 | $this->prepareRequest(), 63 | $this->requestOptions 64 | )->then(function (ResponseInterface $response) { 65 | return $this->jsonDecode( 66 | $response->getBody(), 67 | true 68 | ); 69 | }); 70 | } 71 | 72 | /** 73 | * Prepares a multipart upload request. 74 | * 75 | * @return RequestInterface 76 | */ 77 | private function prepareRequest() 78 | { 79 | $multipartStream = new Psr7\MultipartStream([ 80 | [ 81 | 'name' => 'metadata', 82 | 'headers' => ['Content-Type' => 'application/json; charset=UTF-8'], 83 | 'contents' => $this->jsonEncode($this->metadata) 84 | ], 85 | [ 86 | 'name' => 'data', 87 | 'headers' => ['Content-Type' => $this->contentType], 88 | 'contents' => $this->data 89 | ] 90 | ], 'boundary'); 91 | 92 | $headers = [ 93 | 'Content-Type' => 'multipart/related; boundary=boundary', 94 | ]; 95 | 96 | $size = $multipartStream->getSize(); 97 | if ($size !== null) { 98 | $headers['Content-Length'] = $size; 99 | } 100 | 101 | return new Request( 102 | 'POST', 103 | $this->uri, 104 | $headers, 105 | $multipartStream 106 | ); 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /src/Upload/SignedUrlUploader.php: -------------------------------------------------------------------------------- 1 | headers['Origin'] = $options['origin']; 59 | unset($options['origin']); 60 | } 61 | 62 | parent::__construct($requestWrapper, $data, $uri, $options); 63 | } 64 | 65 | /** 66 | * Creates the resume URI. 67 | * 68 | * @return string 69 | */ 70 | protected function createResumeUri() 71 | { 72 | $headers = $this->headers + [ 73 | 'Content-Type' => $this->contentType, 74 | 'Content-Length' => 0, 75 | 'x-goog-resumable' => 'start' 76 | ]; 77 | 78 | $request = new Request( 79 | 'POST', 80 | $this->uri, 81 | $headers 82 | ); 83 | 84 | $response = $this->requestWrapper->send($request, $this->requestOptions); 85 | 86 | return $this->resumeUri = $response->getHeaderLine('Location'); 87 | } 88 | 89 | /** 90 | * Decode the response body 91 | * 92 | * @param ResponseInterface $response 93 | * @return string 94 | */ 95 | protected function decodeResponse(ResponseInterface $response) 96 | { 97 | return $response->getBody(); 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /src/Upload/StreamableUploader.php: -------------------------------------------------------------------------------- 1 | getResumeUri(); 48 | 49 | if ($writeSize) { 50 | $rangeEnd = $this->rangeStart + $writeSize - 1; 51 | $data = $this->data->read($writeSize); 52 | } else { 53 | $rangeEnd = '*'; 54 | $data = $this->data->getContents(); 55 | $writeSize = strlen($data); 56 | } 57 | 58 | // do the streaming write 59 | $headers = [ 60 | 'Content-Length' => $writeSize, 61 | 'Content-Type' => $this->contentType, 62 | 'Content-Range' => "bytes {$this->rangeStart}-$rangeEnd/*" 63 | ]; 64 | 65 | $request = new Request( 66 | 'PUT', 67 | $resumeUri, 68 | $headers, 69 | $data 70 | ); 71 | 72 | try { 73 | $response = $this->requestWrapper->send($request, $this->requestOptions); 74 | } catch (ServiceException $ex) { 75 | throw new GoogleException( 76 | "Upload failed. Please use this URI to resume your upload: $resumeUri", 77 | $ex->getCode(), 78 | $ex 79 | ); 80 | } 81 | 82 | // reset the buffer with the remaining contents 83 | $this->rangeStart += $writeSize; 84 | 85 | return json_decode($response->getBody(), true); 86 | } 87 | 88 | /** 89 | * Currently only the MultiPartUploader supports async. 90 | * 91 | * Any calls to this will throw a generic Google Exception. 92 | * 93 | * @return PromiseInterface 94 | * @throws GoogleException 95 | * @experimental The experimental flag means that while we believe this method 96 | * or class is ready for use, it may change before release in backwards- 97 | * incompatible ways. Please use with caution, and test thoroughly when 98 | * upgrading. 99 | */ 100 | public function uploadAsync() 101 | { 102 | throw new GoogleException('Currently only the MultiPartUploader supports async.'); 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /src/UriTrait.php: -------------------------------------------------------------------------------- 1 | expand($uri, $variables); 41 | } 42 | 43 | /** 44 | * @param string $uri 45 | * @param array $query 46 | * @return UriInterface 47 | */ 48 | public function buildUriWithQuery($uri, array $query) 49 | { 50 | $query = array_filter($query, function ($v) { 51 | return $v !== null; 52 | }); 53 | 54 | // @todo fix this hack. when using build_query booleans are converted to 55 | // 1 or 0 which the API does not accept. this casts bools to their 56 | // string representation 57 | foreach ($query as $k => &$v) { 58 | if (is_bool($v)) { 59 | $v = $v ? 'true' : 'false'; 60 | } 61 | } 62 | 63 | return Utils::uriFor($uri)->withQuery(Query::build($query)); 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /src/ValidateTrait.php: -------------------------------------------------------------------------------- 1 | 0, 44 | 'nanos' => 0 45 | ]; 46 | 47 | $dt = $this->createDateTimeFromSeconds($timestamp['seconds']); 48 | $nanos = $timestamp['nanos']; 49 | } else { 50 | list($dt, $nanos) = $this->parseTimeString($timestamp); 51 | } 52 | 53 | return new $returnType($dt, $nanos); 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/WhitelistTrait.php: -------------------------------------------------------------------------------- 1 | setMessage('NOTE: Error may be due to Whitelist Restriction. ' . $e->getMessage()); 36 | 37 | return $e; 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /system-bootstrap.php: -------------------------------------------------------------------------------- 1 | register(new MessageAwareArrayComparator()); 10 | \SebastianBergmann\Comparator\Factory::getInstance()->register(new ProtobufMessageComparator()); 11 | \SebastianBergmann\Comparator\Factory::getInstance()->register(new ProtobufGPBEmptyComparator()); 12 | 13 | // Make sure that while testing we bypass the `final` keyword for the GAPIC client. 14 | // Only run this if the individual component has the helper package installed 15 | if (class_exists(BypassFinals::class)) { 16 | BypassFinals::enable(); 17 | } 18 | --------------------------------------------------------------------------------