Repository: shpasser/GaeSupportL5 Branch: master Commit: 392d09f176b1 Files: 32 Total size: 92.3 KB Directory structure: gitextract_vpsw9dwa/ ├── .gitignore ├── .travis.yml ├── LICENSE ├── README.md ├── composer.json ├── phpunit.xml ├── src/ │ ├── Shpasser/ │ │ └── GaeSupportL5/ │ │ ├── Filesystem/ │ │ │ └── GaeAdapter.php │ │ ├── Foundation/ │ │ │ ├── Application.php │ │ │ └── gae_realpath.php │ │ ├── GaeArtisanConsoleServiceProvider.php │ │ ├── GaeSupportServiceProvider.php │ │ ├── Http/ │ │ │ ├── Controllers/ │ │ │ │ └── ArtisanConsoleController.php │ │ │ └── routes.php │ │ ├── Mail/ │ │ │ ├── GaeTransportManager.php │ │ │ ├── MailServiceProvider.php │ │ │ └── Transport/ │ │ │ └── GaeTransport.php │ │ ├── Queue/ │ │ │ ├── GaeConnector.php │ │ │ ├── GaeJob.php │ │ │ ├── GaeQueue.php │ │ │ ├── Listener.php │ │ │ └── QueueServiceProvider.php │ │ ├── Setup/ │ │ │ ├── Configurator.php │ │ │ ├── EnvHelper.php │ │ │ ├── IniHelper.php │ │ │ └── SetupCommand.php │ │ └── Storage/ │ │ ├── CacheFs.php │ │ └── Optimizer.php │ └── views/ │ └── artisan.blade.php └── tests/ ├── .gitkeep └── Shpasser/ └── GaeSupportL5/ └── Setup/ ├── ConfiguratorTest.php ├── FakeCommand.php └── FakeHelpers.php ================================================ FILE CONTENTS ================================================ ================================================ FILE: .gitignore ================================================ /vendor composer.phar composer.lock .DS_Store ================================================ FILE: .travis.yml ================================================ language: php php: - 5.4 - 5.5 - 5.6 - hhvm before_script: - travis_retry composer self-update - travis_retry composer install --prefer-source --no-interaction --dev script: phpunit ================================================ FILE: LICENSE ================================================ The MIT License (MIT) Copyright (c) 2015 Ron Shpasser Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: README.md ================================================ # GaeSupport [![Join the chat at https://gitter.im/shpasser/GaeSupportL5](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/shpasser/GaeSupportL5?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) [![Latest Stable Version](https://poser.pugx.org/shpasser/gae-support-l5/v/stable)](https://packagist.org/packages/shpasser/gae-support-l5) [![Total Downloads](https://poser.pugx.org/shpasser/gae-support-l5/downloads)](https://packagist.org/packages/shpasser/gae-support-l5) [![Latest Unstable Version](https://poser.pugx.org/shpasser/gae-support-l5/v/unstable)](https://packagist.org/packages/shpasser/gae-support-l5) [![License](https://poser.pugx.org/shpasser/gae-support-l5/license)](https://packagist.org/packages/shpasser/gae-support-l5) Google App Engine(GAE) Support package for Laravel 5.1. Currently supported features: - Generation of general configuration files - Mail service provider - Queue service provider - Database connection - Filesystem For Lumen see https://github.com/shpasser/GaeSupportLumen. ## Installation Pull in the package via Composer. ```js "require": { "shpasser/gae-support-l5": "~1.0" } ``` Then include the service provider within `config/app.php`. ```php 'providers' => [ Shpasser\GaeSupportL5\GaeSupportServiceProvider::class ]; ``` ## Usage Generate the GAE related files/entries. Command template: ```bash php artisan gae:setup --config --cache-config --bucket="your-bucket-id" --db-socket="cloud-sql-instance-socket-connection-string" --db-name="cloud-sql-database-name" --db-host="cloud-sql-instance-ipv4-address" app-id ``` Arguments and Options:
php artisan gae:setup [options] [--] app-id

Arguments:
  app-id                     GAE application ID.

Options:
      --config               Generate "app.yaml" and "php.ini" config files.
      --cache-config         Generate cached Laravel config file for use on Google App Engine.
      --bucket=BUCKET        Use the specified GCS-bucket instead of the default one.
      --db-socket=DB-SOCKET  Cloud SQL socket connection string for production environment.
      --db-name=DB-NAME      Cloud SQL database name.
      --db-host=DB-HOST      Cloud SQL database host IPv4 address for local environment.
`--cache-config` option generates cached config file for GAE. This option is essential, because cached config file generated by `php artisan config:cache` is not suitable for use on GAE. As well, cached config file generated for GAE probably will not work in local environment. This option should be used to generate cached config file before application is deployed on GAE. `--bucket` option defines the GCS-bucket ID to be used by the application for storage. Default GCS bucket is configured unless the option is used. When `--db-name` option is defined at least one of `--db-socket` or `--db-host` should be also defined. `--db-socket` is set using the following format: `/cloudsql/:`. Where `` is the Cloud SQL instance name and `` is the name of the application it belongs to. ### Mail The mail driver configuration can be found in `config/mail.php` and `.env.production`, these configuration files are modified / generated by the artisan command. There is no need in any kind of custom configuration. All the outgoing mail messages are sent with sender address of an administrator of the application, i.e. `admin@your-app-id.appspotmail.com`. The `sender`, `to`, `cc`, `bcc`, `replyTo`, `subject`, `body` and `attachment` parts of email message are supported. ### Queues The modified queue configuration file `config/queue.php` should contain: ```php return array( ... /* |-------------------------------------------------------------------------- | GAE Queue Connection |-------------------------------------------------------------------------- | */ 'connections' => array( 'gae' => array( 'driver' => 'gae', 'queue' => 'default', 'url' => '/tasks', 'encrypt' => true, ), ... ), ); ``` The 'default' queue and encryption are used by default. In order to use the queue your `app/Http/routes.php` file should contain the following route: ```php Route::post('tasks', array('as' => 'tasks', function() { return Queue::marshal(); })); ``` This route will be used by the GAE queue to push the jobs. Please notice that the route and the GAE Queue Connection 'url' parameter point to the same URL. Since the requests submitted using the route are issued by GAE itself it cannot be CSRF-protected. For more information on the matter please see http://laravel.com/docs/5.0/queues#push-queues. ### Cache, Session and Log Cache, Session and Log components are supported via the use of specific drivers / handlers: - Cache - using the 'memcached' driver, - Session - using the 'memcached' driver, - Log - using 'syslog' handler. The configuration options for the mentioned drivers / handlers are generated by the artisan command and can be found in `.env.production` configuration file. ### Database Google Cloud SQL is supported via Laravel's MySql driver. The connection configuration is added by the artisan command to `config/database.php` under `cloudsql`. The connection parameters can be configured using `--db-socket`, `--db-name` and `--db-host` options via the artisan command. The database related environment variables are set in `.env.production` and `.env.local` files. The `production` environment is configured to use the socket connection while the `local` configured to connect via the IPv4 address of the Google Cloud SQL instance. Use Google Developers Console in order to obtain the socket connection string and enable the IPv4 address of your database instance. The migrations are supported while working in `local` environment only. To use either the `production` or the `local` environment rename the appropriate file to `.env`. ### Filesystem In order to support Laravel filesystem on GAE the artisan command modifies `config/filesystem.php` to include an additional disk: ```php 'gae' => [ 'driver' => 'gae', 'root' => storage_path().'/app', ], ``` and adds the following line to `.env.production` file: ```php FILESYSTEM = gae ``` ### Optimizations The optimizations allow the application to reduce the use of GCS, which is the only read-write storage available on GAE platform as of now. In order to optimize view compilation the included `cachefs` filesystem can be used to store compiled views using `memcached` service. `cachefs` does not provide the application with a reliable storage solution, information stored using `memcached` is managed according to `memcached` rules and may be deleted when `memcached` decides to. Since the views can be compiled again without any information loss it is appropriate to store compiled views using `cachefs`. `cachefs` has the following structure:
/
+-- bootstrap
    +-- cache
+-- framework
    +-- views
'/framework/views' is used to store the compiled views. Use the following option to enable the feature in `.env.production` and/or `.env.local` file: ```php CACHE_COMPILED_VIEWS = true ``` '/bootstrap/cache' is used to store the `services.json`, `config.php` and `routes.php` files, in order to control caching of these files use the following options in `.env.production` and/or `.env.local` file: ```php CACHE_SERVICES_FILE = true CACHE_CONFIG_FILE = true CACHE_ROUTES_FILE = true ``` In order to use `config.php` first generate it using the `--cache-config` option of `php artisan gae:setup` command. `routes.php` has to be generated using `php artisan route:cache` command. Cache related options are: - supported on GAE and/or in local environment as long as `memcached` service is present, - disabled while executing `php artisan gae:setup` command. Additionally the initialization of GSC bucket can be skipped to boost the performance. In order to do so, set the following option in the `app.yaml` file: ```yaml env_variables: GAE_SKIP_GCS_INIT: true ``` the storage path will be set to `/storage` directory of the GCS bucket and storage directory structure creation will be skipped. If not used the filesystem initialization can be removed to minimize GCS usage. In order to do so, remove the following line from `.env.production` file: ```php FILESYSTEM = gae ``` ### Artisan Console for GAE To support `artisan` commands while running on GAE the package provides `Artisan Console for GAE`. The console is implemented as a separate service and not enabled by default for security reasons. To use the console securely `/artisan` route has to be protected. #### Installation Include the service provider within `config/app.php`. ```php 'providers' => [ Shpasser\GaeSupportL5\GaeArtisanConsoleServiceProvider::class ]; ``` Add `/artisan` URL handler to `app.yaml` file. ```yaml handlers: - url: /artisan script: public/index.php login: admin secure: always - url: /.* script: public/index.php ``` `/artisan` URL handler has to appear before the last one (`url: /.*`), otherwise it will be ignored by GAE. The suggested handler secures the route using GAE URL security options. For more information see https://cloud.google.com/appengine/docs/php/config/appconfig#PHP_app_yaml_Secure_URLs. #### Usage Enter URL http://your-app-id.appspot.com/artisan in your browser and use the displayed form to submit `artisan` commands. Since GAE's filesystem is read-only the commands will not be able to perform write / update operations on it. For the same reason migrations have to be prepared on local development environment before the deployment takes place. Since the console is not really interactive all the commands are executed in non-interactive mode(by automatic appending of `-n` option). ## Deployment Backup the existing `.env` file if needed and rename the generated `.env.production` to `.env` before deploying your app. Download and install GAE SDK for PHP and deploy your app. ## Known Issues As of now Laravel scheduled commands are not supported while running on GAE. In order to use `Artisan Console for GAE` the application class `app/Console/Kernel` has be edited and any of the commands scheduled using its `schedule()` function should be removed. ================================================ FILE: composer.json ================================================ { "name": "shpasser/gae-support-l5", "description": "Google App Engine Support for Laravel 5.1 apps.", "license": "MIT", "authors": [ { "name": "Ron Shpasser", "email": "shpasser@gmail.com" } ], "require": { "php": ">=5.4.0", "illuminate/support": "~5.0", "league/flysystem": "~1.0" }, "require-dev": { "phpunit/phpunit": "~4.4", "illuminate/console": "~5.0" }, "autoload": { "psr-4": { "Shpasser\\": "src/Shpasser" } }, "minimum-stability": "stable" } ================================================ FILE: phpunit.xml ================================================ ./tests/ ================================================ FILE: src/Shpasser/GaeSupportL5/Filesystem/GaeAdapter.php ================================================ applyPathPrefix($path); $this->ensureDirectory(dirname($location)); if (! $stream = fopen($location, 'w')) { return false; } while (! feof($resource)) { fwrite($stream, fread($resource, 1024), 1024); } if (! fclose($stream)) { return false; } if ($visibility = $config->get('visibility')) { $this->setVisibility($path, $visibility); } return compact('path', 'visibility'); } /** * @inheritdoc */ public function applyPathPrefix($path) { $prefixedPath = parent::applyPathPrefix($path); return rtrim($prefixedPath, DIRECTORY_SEPARATOR); } } ================================================ FILE: src/Shpasser/GaeSupportL5/Foundation/Application.php ================================================ gaeBucketPath = null; // Load the 'realpath()' function replacement // for GAE storage buckets. require_once(__DIR__ . '/gae_realpath.php'); $this->detectGae(); if ($this->isRunningOnGae()) { $this->replaceDefaultSymfonyLineDumpers(); } $this->optimizer = new Optimizer($basePath, $this->runningInConsole()); $this->optimizer->bootstrap(); parent::__construct($basePath); } /** * Get the path to the configuration cache file. * * @return string */ public function getCachedConfigPath() { $path = $this->optimizer->getCachedConfigPath(); return $path ?: parent::getCachedConfigPath(); } /** * Get the path to the routes cache file. * * @return string */ public function getCachedRoutesPath() { $path = $this->optimizer->getCachedRoutesPath(); return $path ?: parent::getCachedRoutesPath(); } /** * Get the path to the cached services.json file. * * @return string */ public function getCachedServicesPath() { $path = $this->optimizer->getCachedServicesPath(); if ($path) { return $path; } if ($this->isRunningOnGae()) { return $this->storagePath().'/framework/services.json'; } return parent::getCachedServicesPath(); } /** * Detect if the application is running on GAE. */ protected function detectGae() { if (! class_exists(self::GAE_ID_SERVICE)) { $this->runningOnGae = false; $this->appId = null; return; } $AppIdentityService = self::GAE_ID_SERVICE; $this->appId = $AppIdentityService::getApplicationId(); $this->runningOnGae = ! preg_match('/dev~/', getenv('APPLICATION_ID')); } /** * Replaces the default output stream of Symfony's * CliDumper and HtmlDumper classes in order to * be able to run on Google App Engine. * * 'php://stdout' is used by CliDumper, * 'php://output' is used by HtmlDumper, * both are not supported on GAE. */ protected function replaceDefaultSymfonyLineDumpers() { HtmlDumper::$defaultOutput = CliDumper::$defaultOutput = function ($line, $depth, $indentPad) { if (-1 !== $depth) { echo str_repeat($indentPad, $depth).$line.PHP_EOL; } }; } /** * Returns 'true' if running on GAE. * * @return bool */ public function isRunningOnGae() { return $this->runningOnGae; } /** * Returns the GAE app ID. * * @return string */ public function getGaeAppId() { return $this->appId; } /** * Override the storage path * * @return string Storage path URL */ public function storagePath() { if ($this->runningOnGae) { if (! is_null($this->gaeBucketPath)) { return $this->gaeBucketPath; } $buckets = ini_get('google_app_engine.allow_include_gs_buckets'); // Get the first bucket in the list. $bucket = current(explode(', ', $buckets)); if ($bucket) { $this->gaeBucketPath = "gs://{$bucket}/storage"; if (env('GAE_SKIP_GCS_INIT')) { return $this->gaeBucketPath; } if (! file_exists($this->gaeBucketPath)) { mkdir($this->gaeBucketPath); mkdir($this->gaeBucketPath.'/app'); mkdir($this->gaeBucketPath.'/framework'); mkdir($this->gaeBucketPath.'/framework/views'); } return $this->gaeBucketPath; } } return parent::storagePath(); } } ================================================ FILE: src/Shpasser/GaeSupportL5/Foundation/gae_realpath.php ================================================ loadViewsFrom(__DIR__.'/../../views', 'gae-support-l5'); if (! $this->app->routesAreCached()) { require __DIR__.'/Http/routes.php'; } } /** * Register the service provider. * * @return void */ public function register() { // } /** * Get the services provided by the provider. * * @return array */ public function provides() { return array('gae-artisan-console'); } } ================================================ FILE: src/Shpasser/GaeSupportL5/GaeSupportServiceProvider.php ================================================ app->singleton('gae.setup', function ($app) { return new SetupCommand; }); $this->commands('gae.setup'); } /** * Get the services provided by the provider. * * @return array */ public function provides() { return array('gae-support'); } } ================================================ FILE: src/Shpasser/GaeSupportL5/Http/Controllers/ArtisanConsoleController.php ================================================ input('command'); if ($command === '') { $command = 'list'; } $output= new BufferedOutput; Artisan::handle(new StringInput($command.self::NON_INTERACTIVE), $output); $results = $output->fetch(); return view('gae-support-l5::artisan', compact(['command', 'results'])); } } ================================================ FILE: src/Shpasser/GaeSupportL5/Http/routes.php ================================================ 'artisan', 'uses' => ArtisanConsoleController::class.'@show')); Route::post('artisan', array('as' => 'artisan', 'uses' => ArtisanConsoleController::class.'@execute')); ================================================ FILE: src/Shpasser/GaeSupportL5/Mail/GaeTransportManager.php ================================================ app); } } ================================================ FILE: src/Shpasser/GaeSupportL5/Mail/MailServiceProvider.php ================================================ app->isRunningOnGae()) { $this->app['swift.transport'] = $this->app->share(function ($app) { return new GaeTransportManager($app); }); } else { parent::registerSwiftTransport(); } } } ================================================ FILE: src/Shpasser/GaeSupportL5/Mail/Transport/GaeTransport.php ================================================ app = $app; } /** * {@inheritdoc} */ public function isStarted() { return true; } /** * {@inheritdoc} */ public function start() { return true; } /** * {@inheritdoc} */ public function stop() { return true; } /** * {@inheritdoc} */ public function send(Swift_Mime_Message $message, &$failedRecipients = null) { try { $to = implode(', ', array_keys((array) $message->getTo())); $cc = implode(', ', array_keys((array) $message->getCc())); $bcc = implode(', ', array_keys((array) $message->getBcc())); $replyto = ''; foreach ((array) $message->getReplyTo() as $address => $name) { $replyto = $address; break; } $mail_options = [ "sender" => "admin@{$this->app->getGaeAppId()}.appspotmail.com", "to" => $to, "subject" => $message->getSubject(), "htmlBody" => $message->getBody() ]; if ($cc !== '') { $mail_options['cc'] = $cc; } if ($bcc !== '') { $mail_options['bcc'] = $bcc; } if ($replyto !== '') { $mail_options['replyto'] = $replyto; } $attachments = $this->getAttachmentsArray($message); if (count($attachments) > 0) { $mail_options['attachments'] = $attachments; } $gae_message = new GAEMessage($mail_options); $gae_message->send(); } catch (InvalidArgumentException $ex) { Log::warning("Exception sending mail: ".$ex); } } /** * {@inheritdoc} */ public function registerPlugin(Swift_Events_EventListener $plugin) { // } /** * Returns an array of attachments extracted from * a given mail message. * * @param $message * * @return array */ protected function getAttachmentsArray($message) { $attachments = array(); foreach ($message->getChildren() as $entity) { if ($entity instanceof Swift_Attachment) { $attachments[] = array( 'name' => $entity->getFilename(), 'data' => $entity->getBody(), 'content_id' => "<{$entity->getContentType()}>" ); } } return $attachments; } } ================================================ FILE: src/Shpasser/GaeSupportL5/Queue/GaeConnector.php ================================================ crypt = $crypt; $this->request = $request; } /** * Establish a queue connection. * * @param array $config * @return \Illuminate\Queue\QueueInterface */ public function connect(array $config) { return new GaeQueue($this->request, $config['queue'], $config['url'], $config['encrypt']); } } ================================================ FILE: src/Shpasser/GaeSupportL5/Queue/GaeJob.php ================================================ job = $job; $this->gaeQueue = $gaeQueue; $this->pushed = $pushed; $this->container = $container; } /** * Fire the job. * * @return void */ public function fire() { $this->resolveAndFire(json_decode($this->getRawBody(), true)); } /** * Get the raw body string for the job. * * @return string */ public function getRawBody() { return $this->job->body; } /** * Delete the job from the queue. * * @return void */ public function delete() { parent::delete(); if (isset($this->job->pushed)) { return; } } /** * Release the job back into the queue. * * @param int $delay * @return void */ public function release($delay = 0) { if (! $this->pushed) { $this->delete(); } $this->recreateJob($delay); } /** * Release a pushed job back onto the queue. * * @param int $delay * @return void */ protected function recreateJob($delay) { $payload = json_decode($this->job->body, true); array_set($payload, 'attempts', array_get($payload, 'attempts', 1) + 1); $this->gaeQueue->recreate(json_encode($payload), $this->getQueue(), $delay); } /** * Get the number of times the job has been attempted. * * @return int */ public function attempts() { return array_get(json_decode($this->job->body, true), 'attempts', 1); } /** * Get the job identifier. * * @return string */ public function getJobId() { return $this->job->id; } /** * Get the IoC container instance. * * @return \Illuminate\Container\Container */ public function getContainer() { return $this->container; } /** * Get the underlying Gae queue instance. * * @return \Shpasser\GaeSupportL5\Queue\GaeQueue */ public function getGaeQueue() { return $this->gaeQueue; } /** * Get the underlying Gae job. * * @return array */ public function getGaeJob() { return $this->job; } /** * Get the name of the queue the job belongs to. * * @return string */ public function getQueue() { return array_get(json_decode($this->job->body, true), 'queue'); } } ================================================ FILE: src/Shpasser/GaeSupportL5/Queue/GaeQueue.php ================================================ request = $request; $this->url = $url; $this->default = $default; $this->shouldEncrypt = $shouldEncrypt; } /** * Push a new job onto the queue. * * @param string $job * @param mixed $data * @param string $queue * @return mixed */ public function push($job, $data = '', $queue = null) { return $this->pushRaw($this->createPayload($job, $data, $queue), $queue); } /** * Push a raw payload onto the queue. * * @param string $payload * @param string $queue * @param array $options * @return mixed */ public function pushRaw($payload, $queue = null, array $options = array()) { if ($this->shouldEncrypt) { $payload = $this->encrypter->encrypt($payload); } $task = new PushTask($this->url, array(self::PAYLOAD_REQ_PARAM_NAME => $payload), $options); return $task->add($this->getQueue($queue)); } /** * Push a raw payload onto the queue after encrypting the payload. * * @param string $payload * @param string $queue * @param int $delay * @return mixed */ public function recreate($payload, $queue = null, $delay) { $options = array('delay_seconds' => $this->getSeconds($delay)); return $this->pushRaw($payload, $queue, $options); } /** * Push a new job onto the queue after a delay. * * @param \DateTime|int $delay * @param string $job * @param mixed $data * @param string $queue * @return mixed */ public function later($delay, $job, $data = '', $queue = null) { $delay_seconds = $this->getSeconds($delay); $payload = $this->createPayload($job, $data, $queue); return $this->pushRaw($payload, $queue, compact('delay_seconds')); } /** * Pop the next job off of the queue. * * @param string $queue * @return \Illuminate\Queue\Jobs\Job|null */ public function pop($queue = null) { throw new \RuntimeException("Pop is not supported by GaeQueue."); } /** * Delete a message from the Gae queue. * * @param string $queue * @param string $id * @return void */ public function deleteMessage($queue, $id) { throw new \RuntimeException("Delete is not supported by GaeQueue."); } /** * Marshal a push queue request and fire the job. * * @return \Illuminate\Http\Response */ public function marshal() { try { $job = $this->marshalPushedJob(); } catch (\Exception $e) { // Ignore for security reasons! // So if we are being hacked // the hacker would think it went OK. Log::warning('Marshalling Queue Request: Invalid job.'); return new Response('OK'); } if (isset($job->id)) { $this->createPushedGaeJob($job)->fire(); } else { Log::warning('Marshalling Queue Request: No GAE header supplied.'); } return new Response('OK'); } /** * Marshal out the pushed job and payload. * * @return object */ protected function marshalPushedJob() { $r = $this->request; $body = $this->parseJobBody($r->input(self::PAYLOAD_REQ_PARAM_NAME)); return (object) array( 'id' => $r->header('X-AppEngine-TaskName'), 'body' => $body, 'pushed' => true, ); } /** * Create a new GaeJob for a pushed job. * * @param object $job * @return \Shpasser\GaeSupportL5\Queue\GaeJob */ protected function createPushedGaeJob($job) { return new GaeJob($this->container, $this, $job, true); } /** * Create a payload string from the given job and data. * * @param string $job * @param mixed $data * @param string $queue * @return string */ protected function createPayload($job, $data = '', $queue = null) { $payload = $this->setMeta(parent::createPayload($job, $data), 'attempts', 1); return $this->setMeta($payload, 'queue', $this->getQueue($queue)); } /** * Parse the job body for firing. * * @param string $body * @return string */ protected function parseJobBody($body) { return $this->shouldEncrypt ? $this->encrypter->decrypt($body) : $body; } /** * Get the queue or return the default. * * @param string|null $queue * @return string */ public function getQueue($queue) { return $queue ?: $this->default; } /** * Get the request instance. * * @return \Illuminate\Http\Request */ public function getRequest() { return $this->request; } /** * Set the request instance. * * @param \Illuminate\Http\Request $request */ public function setRequest(Request $request) { $this->request = $request; } } ================================================ FILE: src/Shpasser/GaeSupportL5/Queue/Listener.php ================================================ isRunningOnGae()) { return parent::buildWorkerCommand(); } $binary = (new PhpExecutableFinder)->find(false); if (defined('HHVM_VERSION')) { $binary .= ' --php'; } if (defined('ARTISAN_BINARY')) { $artisan = ARTISAN_BINARY; } else { $artisan = 'artisan'; } $command = 'queue:work %s --queue=%s --delay=%s --memory=%s --sleep=%s --tries=%s'; return "{$binary} {$artisan} {$command}"; } } ================================================ FILE: src/Shpasser/GaeSupportL5/Queue/QueueServiceProvider.php ================================================ registerGaeConnector($manager); } /** * Register the GAE queue connector. * * @param \Illuminate\Queue\QueueManager $manager * @return void */ protected function registerGaeConnector($manager) { $app = $this->app; $manager->addConnector('gae', function () use ($app) { return new GaeConnector($app['encrypter'], $app['request']); }); } } ================================================ FILE: src/Shpasser/GaeSupportL5/Setup/Configurator.php ================================================ myCommand = $myCommand; } /** * Configures a Laravel app to be deployed on GAE. * * @param string $appId the GAE application ID. * @param bool $generateConfig if 'true' => generate GAE config files(app.yaml and php.ini). * @param bool $cacheConfig if 'true' => generate cached config file(config.php). * @param string $bucketId the custom GCS bucket ID, if 'null' the default bucket is used. * @param string $dbSocket Cloud SQL socket connection string. * @param string $dbName Cloud SQL database name. * @param string $dbHost Cloud SQL host IPv4 address. */ public function configure($appId, $generateConfig, $cacheConfig, $bucketId, $dbSocket, $dbName, $dbHost) { $env_file = app_path().'/../.env'; $env_production_file = app_path().'/../.env.production'; $env_local_file = app_path().'/../.env.local'; $bootstrap_app_php = app_path().'/../bootstrap/app.php'; $config_app_php = app_path().'/../config/app.php'; $config_view_php = app_path().'/../config/view.php'; $config_queue_php = app_path().'/../config/queue.php'; $config_database_php = app_path().'/../config/database.php'; $config_filesystems_php = app_path().'/../config/filesystems.php'; $cached_config_php = base_path().'/bootstrap/cache/config.php'; $this->createEnvProductionFile($env_file, $env_production_file, $dbSocket, $dbName); $this->createEnvLocalFile($env_file, $env_local_file, $dbHost, $dbName); $this->processFile($bootstrap_app_php, ['replaceAppClass']); $this->processFile($config_app_php, [ 'replaceLaravelServiceProviders', 'setLogHandler' ]); $this->processFile($config_view_php, ['replaceCompiledPath']); $this->processFile($config_queue_php, ['addQueueConfig']); $this->processFile($config_database_php, ['addCloudSqlConfig']); $this->processFile($config_filesystems_php, ['addGaeDisk']); if ($cacheConfig) { $env = new Dotenv(dirname($env_production_file), basename($env_production_file)); $env->overload(); $result = Artisan::call('config:cache', array()); if ($result === 0) { $this->processFile($cached_config_php, ['fixCachedConfig']); } } if ($generateConfig) { $app_yaml = app_path().'/../app.yaml'; $publicPath = app_path().'/../public'; $php_ini = app_path().'/../php.ini'; $this->generateAppYaml($appId, $app_yaml, $publicPath); $this->generatePhpIni($appId, $bucketId, $php_ini); } } /** * Creates a '.env.production' file based on the existing '.env' file. * * @param string $env_file The '.env' file path. * @param string $env_production_file The '.env.production' file path. * @param string $dbSocket Cloud SQL socket connection string. * @param string $dbName Cloud SQL database name. */ protected function createEnvProductionFile($env_file, $env_production_file, $dbSocket, $dbName) { if (!file_exists($env_file)) { $this->myCommand->error('Cannot find ".env" file to import the existing options.'); return; } if (file_exists($env_production_file)) { $overwrite = $this->myCommand->confirm( 'Overwrite the existing ".env.production" file?', false ); if (! $overwrite) { return; } } $env = new EnvHelper; $env->read($env_file); $env['APP_ENV'] = 'production'; $env['APP_DEBUG'] = 'false'; $env['APP_LOG'] = 'syslog'; $env['CACHE_DRIVER'] = 'memcached'; $env['SESSION_DRIVER'] = 'memcached'; $env['MAIL_DRIVER'] = 'gae'; $env['QUEUE_DRIVER'] = 'gae'; $env['FILESYSTEM'] = 'gae'; if ((! is_null($dbSocket)) && (! is_null($dbName))) { $env['DB_CONNECTION'] = 'cloudsql'; $env['DB_SOCKET'] = $dbSocket; $env['DB_HOST'] = ''; $env['DB_DATABASE'] = $dbName; $env['DB_USERNAME'] = 'root'; $env['DB_PASSWORD'] = ''; } $this->addOptimizerOptions($env); $env->write($env_production_file); $this->myCommand->info('Created the ".env.production" file.'); } /** * Creates a '.env.local' file based on the existing '.env' file. * * @param string $env_file The '.env' file path. * @param string $env_local_file The '.env.local' file path. * @param string $dbHost Cloud SQL host IPv4 address. * @param string $dbName Cloud SQL database name. */ protected function createEnvLocalFile($env_file, $env_local_file, $dbHost, $dbName) { if (!file_exists($env_file)) { $this->myCommand->error('Cannot find ".env" file to import the existing options.'); return; } if (file_exists($env_local_file)) { $overwrite = $this->myCommand->confirm( 'Overwrite the existing ".env.local" file?', false ); if (! $overwrite) { return; } } $env = new EnvHelper; $env->read($env_file); $env['APP_ENV'] = 'local'; $env['APP_DEBUG'] = 'true'; $env['CACHE_DRIVER'] = 'file'; $env['SESSION_DRIVER'] = 'file'; if ((! is_null($dbHost)) && (! is_null($dbName))) { $env['DB_CONNECTION'] = 'cloudsql'; $env['DB_SOCKET'] = ''; $env['DB_HOST'] = $dbHost; $env['DB_DATABASE'] = $dbName; $env['DB_USERNAME'] = 'root'; $env['DB_PASSWORD'] = 'password'; } $this->addOptimizerOptions($env); $env->write($env_local_file); $this->myCommand->info('Created the ".env.local" file.'); } /** * Adds 'Optimizer' options to an environment object. * * @param \Shpasser\GaeSupportL5\Setup\EnvHelper $env * the environment object to modify. */ protected function addOptimizerOptions(EnvHelper $env) { $env['CACHE_SERVICES_FILE'] = 'false'; $env['CACHE_CONFIG_FILE'] = 'false'; $env['CACHE_ROUTES_FILE'] = 'false'; $env['CACHE_COMPILED_VIEWS'] = 'false'; } /** * Processes a given file with given processors. * * @param string $filePath the path of the file to be processed. * @param array $processors array of processor function names to * be called during the file processing. Every such function shall * receive the file contents string as a parameter and return the * modified file contents. * * * protected function processorFunc($contents) * { * ... * return $modified; * } * */ protected function processFile($filePath, $processors) { $contents = file_get_contents($filePath); $processed = $contents; foreach ($processors as $processor) { $processed = $this->$processor($processed); } if ($processed === $contents) { return; } $this->backupFile($filePath); file_put_contents($filePath, $processed); } /** * Processor function. Replaces the Laravel * application class with the one compatible with GAE. * * @param string $contents the 'bootstrap/app.php' file contents. * * @return string the modified file contents. */ protected function replaceAppClass($contents) { $modified = str_replace( 'Illuminate\Foundation\Application', 'Shpasser\GaeSupportL5\Foundation\Application', $contents); if ($contents !== $modified) { $this->myCommand->info('Replaced the application class in "bootstrap/app.php".'); } return $modified; } /** * Processor function. Replaces the Laravel * service providers with GAE compatible ones. * * @param string $contents the 'config/app.php' file contents. * * @return string the modified file contents. */ protected function replaceLaravelServiceProviders($contents) { $strings = [ 'Illuminate\Mail\MailServiceProvider', 'Illuminate\Queue\QueueServiceProvider' ]; // Replacement to: // - additionally support Google App Engine Queues, // - additionally support Google App Engine Mail. $replacements = [ 'Shpasser\GaeSupportL5\Mail\MailServiceProvider', 'Shpasser\GaeSupportL5\Queue\QueueServiceProvider' ]; $modified = str_replace($strings, $replacements, $contents); if ($contents !== $modified) { $this->myCommand->info('Replaced the service providers in "config/app.php".'); } return $modified; } /** * Processor function. Sets the syslog log handler * for a Laravel GAE app. * * @param string $contents the 'config/app.php' file contents. * * @return string the modified file contents. */ protected function setLogHandler($contents) { $expression = "/'log'.*=>((?!env\('APP_LOG').)*'\b.+?\b'\)?/"; $replacement = "'log' => env('APP_LOG', 'single')"; $modified = preg_replace($expression, $replacement, $contents); if ($contents !== $modified) { $this->myCommand->info('Set the log handler in "config/app.php".'); } return $modified; } /** * Processor function. Replaces 'compiled' path with GAE * compatible one when running on GAE. * * @param string $contents the 'config/view.php' file contents. * @return string the modified file contents. */ protected function replaceCompiledPath($contents) { $expression = "/'compiled'\s*=>\s*(.+(,|(?=\s+\]))|[^,]+(,|(?=\s+\])))/"; $replacement = << env('CACHE_COMPILED_VIEWS') ? Shpasser\GaeSupportL5\Storage\Optimizer::COMPILED_VIEWS_PATH : storage_path('framework/views'), EOT; $modified = preg_replace($expression, $replacement, $contents); if ($contents !== $modified) { $this->myCommand->info('Replaced the \'compiled\' path in "config/view.php".'); } return $modified; } /** * Adds the GAE queue configuration to the 'config/queue.php' * if it does not already exist. * * @param string $contents the 'config/queue.php' file contents. * @return string the modified file contents. */ protected function addQueueConfig($contents) { if (str_contains($contents, "'gae'")) { return $contents; } $expression = "/'connections'\s*=>\s*\[/"; $replacement = << [ 'gae' => [ 'driver' => 'gae', 'queue' => 'default', 'url' => '/tasks', 'encrypt' => true, ], EOT; $modified = preg_replace($expression, $replacement, $contents); if ($contents !== $modified) { $this->myCommand->info('Added queue driver configuration in "config/queue.php".'); } return $modified; } /** * Adds the Cloud SQL configuration to the 'config/database.php' * if it does not already exist. * * @param string $contents the 'config/database.php' file contents. * @return string the modified file contents. */ protected function addCloudSqlConfig($contents) { if (str_contains($contents, "'cloudsql'")) { return $contents; } $expressions = [ "/'default'.*=>\s*'\b.+\b'/", "/'connections'\s*=>\s*\[/" ]; $replacements = [ "'default' => env('DB_CONNECTION', 'mysql')", << [ 'cloudsql' => [ 'driver' => 'mysql', 'unix_socket' => env('DB_SOCKET'), 'host' => env('DB_HOST'), 'database' => env('DB_DATABASE'), 'username' => env('DB_USERNAME'), 'password' => env('DB_PASSWORD'), 'charset' => 'utf8', 'collation' => 'utf8_unicode_ci', 'prefix' => '', 'strict' => false, ], EOT ]; $modified = preg_replace($expressions, $replacements, $contents); if ($contents !== $modified) { $this->myCommand->info('Added Cloud SQL driver configuration in "config/database.php".'); } return $modified; } /** * Adds the GAE disk configuration to the 'config/filesystem.php' * if it does not already exist. * * @param string $contents the 'config/filesystem.php' file contents. * @return string the modified file contents. */ protected function addGaeDisk($contents) { if (str_contains($contents, "'gae'")) { return $contents; } $expressions = [ "/'default'.*=>\s*'\b.+\b'/", "/'disks'\s*=>\s*\[/" ]; $replacements = [ "'default' => env('FILESYSTEM', 'local')", << [ 'gae' => [ 'driver' => 'gae', 'root' => storage_path().'/app', ], EOT ]; $modified = preg_replace($expressions, $replacements, $contents); if ($contents !== $modified) { $this->myCommand->info('Added GAE filesystem driver configuration in "config/filesystems.php".'); } return $modified; } /** * Processor function. Pre-processes windows paths. * * @param string $contents the 'bootstrap/cache/config.php' file contents. * * @return string the modified file contents. */ protected function preprocessWindowsPaths($contents) { $expression = "/'([A-Za-z]:)?((\\\\|\/)[^\\/:*?\"\'<>|\r\n]*)*'/"; $paths = array(); preg_match_all($expression, $contents, $paths); $modified = $contents; foreach ($paths[0] as $path) { $normalizedPath = str_replace('\\\\', '/', $path); $modified = str_replace($path, $normalizedPath, $modified); } if ($contents !== $modified) { $this->myCommand->info('Preprocessed windows paths in "bootstrap/cache/config.php".'); } return $modified; } /** * Fixes the paths in the cached config file. * * @param string $contents the 'bootstrap/cache/config.php' file contents. * @return string the modified file contents. */ protected function fixCachedConfig($contents) { $app_path = app_path(); $storage_path = storage_path(); $base_path = base_path(); $replaceFunction = 'str_replace'; if ($this->isRunningOnWindows()) { $contents = $this->preprocessWindowsPaths($contents); $app_path = str_replace('\\', '/', $app_path); $storage_path = str_replace('\\', '/', $storage_path); $base_path = str_replace('\\', '/', $base_path); $replaceFunction = 'str_ireplace'; } $strings = [ "'${app_path}", "'${storage_path}", "'${base_path}" ]; $replacements = [ "app_path().'", "storage_path().'", "base_path().'" ]; $modified = $replaceFunction($strings, $replacements, $contents); if ($contents !== $modified) { $this->myCommand->info('Generated "bootstrap/cache/config.php" for GAE deployment.'); $this->myCommand->comment('* To use "bootstrap/cache/config.php" locally please regenerate it.'); } return $modified; } /** * Generates a "app.yaml" file for a GAE app. * * @param string $appId the GAE app id. * @param string $filePath the 'app.yaml' file path. * @param string $publicPath the application public dir path. */ protected function generateAppYaml($appId, $filePath, $publicPath) { if (file_exists($filePath)) { $overwrite = $this->myCommand->confirm( 'Overwrite the existing "app.yaml" file?', false ); if (! $overwrite) { return; } } $pathMappings = ''; foreach (new \DirectoryIterator($publicPath) as $fileInfo) { if ($fileInfo->isDot() || ! $fileInfo->isDir()) { continue; } $dirName = $fileInfo->getFilename(); // Elixir's 'build' directory // is added anyway -> skip it if ($dirName === "build") { continue; } $pathMappings .= PHP_EOL. <<myCommand->info('Generated the "app.yaml" file.'); } /** * Generates a "php.ini" file for a GAE app. * * @param string $appId the GAE app id. * @param string $bucketId the GAE gs-bucket id. * @param string $filePath the 'php.ini' file path. */ protected function generatePhpIni($appId, $bucketId, $filePath) { if (file_exists($filePath)) { $overwrite = $this->myCommand->confirm( 'Overwrite the existing "php.ini" file?', false ); if (! $overwrite) { return; } } $storageBucket = "{$appId}.appspot.com"; if ($bucketId !== null) { $storageBucket = $bucketId; } $contents = <<myCommand->info('Generated the "php.ini" file.'); } /** * Determines whether the app is running on windows. * @return boolean 'true' if running on Windows, otherwise 'false'. */ protected function isRunningOnWindows() { if (strtoupper(substr(PHP_OS, 0, 3)) === 'WIN') { return true; } return false; } /** * Creates a backup copy of a desired file. * * @param string $filePath the file path. * @return string the created backup file path. */ protected function backupFile($filePath) { $sourcePath = $filePath; $backupPath = $filePath.'.bak'; if (file_exists($backupPath)) { $date = new \DateTime(); $backupPath = "{$filePath}{$date->getTimestamp()}.bak"; } copy($sourcePath, $backupPath); return $backupPath; } /** * Restores a file from its backup copy. * * @param string $filePath the file path. * @param string $backupPath the backup path. * @param boolean $clean if 'true' deletes the backup copy. * @return string the created backup file path. */ protected function restoreFile($filePath, $backupPath, $clean = true) { if (file_exists($backupPath)) { copy($backupPath, $filePath); if ($clean) { unlink($backupPath); } } return $backupPath; } } ================================================ FILE: src/Shpasser/GaeSupportL5/Setup/EnvHelper.php ================================================ filePath = $file; $this->lines = file($this->filePath, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES); } /** * Writes the configuration data back to the ENV file. * * @param string $file If not empty will be used as an * ENV file path to be written. */ public function write($file = "") { $envString = $this->generateEnvString($this->lines); file_put_contents($file ? $file : $this->filePath, $envString); } /** * Generates ENV string from a given associative array. * * @param array $array the array containing ENV data. * @return string */ protected function generateEnvString($array) { $envString = ""; foreach ($array as $value) { $envString .= "{$value}".PHP_EOL; } return $envString; } /** * Parses key and value for a given line. * * @param string $line the line to be parsed. * @param string &$key the parsed key. * @param string &$value the parsed value. * @return boolean true if parsing was successful, false otherwise. */ protected function parseEnvLine($line, &$key, &$value) { $parseOk = false; $keyExpr = '/(^\S+)\s*=\s*/'; if (preg_match($keyExpr, $line, $matched) === 1) { $key = $matched[1]; $value = substr($line, strlen($matched[0])); $parseOk = true; } return $parseOk; } /** * Finds the line containing the given key. * * @param string $key the key. * @return integer the line index > 0, otherwise -1. */ protected function findLine($key) { $parsedKey = null; $parsedValue = null; foreach ($this->lines as $index => $line) { $parseOk = $this->parseEnvLine($line, $parsedKey, $parsedValue); if ($parseOk && $key === $parsedKey) { return $index; } } return -1; } /** * Assigns a value to the specified offset * * @param string $offset The offset to assign the value to * @param mixed $value The value to set * @access public * @abstracting ArrayAccess */ public function offsetSet($offset, $value) { if (is_null($offset)) { $this->lines[] = $value; } else { $index = $this->findLine($offset); if ($index >= 0) { $this->lines[$index] = "{$offset}={$value}"; } else { $this->lines[] = "{$offset}={$value}"; } } } /** * Whether or not an offset exists * * @param string $offset An offset to check for * @access public * @return boolean * @abstracting ArrayAccess */ public function offsetExists($offset) { $lineFound = false; $index = $this->findLine($offset); if ($index >= 0) { $lineFound = true; } return $lineFound; } /** * Un-sets an offset * * @param string $offset The offset to unset * @access public * @abstracting ArrayAccess */ public function offsetUnset($offset) { $index = $this->findLine($offset); if ($index >= 0) { unset($this->lines[$index]); } } /** * Returns the value at specified offset * * @param string $offset The offset to retrieve * @access public * @return mixed * @abstracting ArrayAccess */ public function offsetGet($offset) { $index = $this->findLine($offset); if ($index > 0) { $parsedKey = null; $parsedValue = null; $parseOk = $this->parseEnvLine($this->lines[$index], $parsedKey, $parsedValue); return $parseOk ? $parsedValue : null; } return null; } } ================================================ FILE: src/Shpasser/GaeSupportL5/Setup/IniHelper.php ================================================ filePath = $file; $this->config = parse_ini_file($this->filePath); } /** * Writes the configuration data back to the INI file. * * @param string $file If not empty will be used as an * INI file path to be written. */ public function write($file = "") { $iniString = $this->generateIniString($this->config); file_put_contents($file ? $file : $this->filePath, $iniString); } /** * Generates an INI string from a given associative array. * * @param array $array the array containing the INI data. * @return string */ protected function generateIniString($array) { $iniString = ""; foreach ($array as $key => $value) { if (is_array($value)) { $iniString .= "[{$key}]".PHP_EOL; $iniString .= $this->generateIniString($value); } else { $iniString .= "{$key}={$value}".PHP_EOL; } } return $iniString; } /** * Assigns a value to the specified offset * * @param string $offset The offset to assign the value to * @param mixed $value The value to set * @access public * @abstracting ArrayAccess */ public function offsetSet($offset, $value) { if (is_null($offset)) { $this->config[] = $value; } else { $this->config[$offset] = $value; } } /** * Whether or not an offset exists * * @param string $offset An offset to check for * @access public * @return boolean * @abstracting ArrayAccess */ public function offsetExists($offset) { return isset($this->config[$offset]); } /** * Un-sets an offset * * @param string $offset The offset to unset * @access public * @abstracting ArrayAccess */ public function offsetUnset($offset) { unset($this->config[$offset]); } /** * Returns the value at specified offset * * @param string $offset The offset to retrieve * @access public * @return mixed * @abstracting ArrayAccess */ public function offsetGet($offset) { return isset($this->config[$offset]) ? $this->config[$offset] : null; } } ================================================ FILE: src/Shpasser/GaeSupportL5/Setup/SetupCommand.php ================================================ fire(); } /** * Execute the console command. * For backward compatibility. * * @return mixed */ public function fire() { $dbSocket = $this->option('db-socket'); $dbHost = $this->option('db-host'); $dbName = $this->option('db-name'); if (! is_null($dbName) && (is_null($dbSocket) && is_null($dbHost))) { $this->error("Option '--db-name' requires at least one of: '--db-socket' OR '--db-host' to be defined."); return; } $configurator = new Configurator($this); $configurator->configure( $this->argument('app-id'), $this->option('config'), $this->option('cache-config'), $this->option('bucket'), $this->option('db-socket'), $this->option('db-name'), $this->option('db-host') ); } /** * Get the console command arguments. * * @return array */ protected function getArguments() { return array( array('app-id', InputArgument::REQUIRED, 'GAE application ID.'), ); } /** * Get the console command options. * * @return array */ protected function getOptions() { return array( array('config', null, InputOption::VALUE_NONE, 'Generate "app.yaml" and "php.ini" config files.', null), array('cache-config', null, InputOption::VALUE_NONE, 'Generate cached Laravel config file for use on Google App Engine.', null), array('bucket', null, InputOption::VALUE_REQUIRED, 'Use the specified gs-bucket instead of the default one.', null), array('db-socket', null, InputOption::VALUE_REQUIRED, 'Cloud SQL socket connection string for production environment.', null), array('db-name', null, InputOption::VALUE_REQUIRED, 'Cloud SQL database name.', null), array('db-host', null, InputOption::VALUE_REQUIRED, 'Cloud SQL database host IPv4 address for local environment.', null), ); } } ================================================ FILE: src/Shpasser/GaeSupportL5/Storage/CacheFs.php ================================================ '127.0.0.1', 'port' => 11211, 'weight' => 100]]; self::$memcached = new Memcached(); foreach ($servers as $server) { self::$memcached->addServer( $server['host'], $server['port'], $server['weight'] ); } if (self::$memcached->getVersion() === false) { throw new \RuntimeException("Could not establish Memcached connection."); } } return self::$memcached; } private function dir_list($path) { $dirPath = $path.'/'; $length = strlen($dirPath); $paths = array_merge(self::cache()->getAllKeys(), self::$folders); $dir = array_filter($paths, function ($file) use ($dirPath,$length) { if (substr($file, 0, $length) === $dirPath && strrpos($file, '/') == $length) { return true; } return false; }); return $dir; } /** * Constructs a new stream wrapper. */ public function __construct() { $this->fd = null; $this->mode = null; $this->path = null; } /** * Destructs an existing stream wrapper. */ public function __destruct() { } /** * Renames a storage object. * * @return true if the object was renamed, false otherwise */ public function rename($from, $to) { // TODO: add directories support $contents = self::cache()->get($from); if (false === $contents) { return false; } self::cache()->delete($from); self::cache()->set($to, $contents); return true; } /** * Closes the stream */ public function stream_close() { $this->stream_flush(); fclose($this->fd); $this->path = null; $this->fd = null; } /** * Tests for end-of-file on a file pointer. * * @return true if the read/write position is at the end of the stream and if * no more data is available to be read, otherwise false */ public function stream_eof() { return feof($this->fd); } /** * Flushes the output. * * @return true if the cached data was successfully stored (or if there was * no data to store), false if the data could not be stored. */ public function stream_flush() { switch ($this->mode) { case 'r+': case 'rb+': case 'w': case 'wb': case 'w+': case 'wb+': case 'a': case 'ab': case 'a+': case 'ab+': $origPos = ftell($this->fd); fseek($this->fd, 0, SEEK_END); $size = ftell($this->fd); fseek($this->fd, 0, SEEK_SET); $contents = fread($this->fd, $size); fseek($this->fd, $origPos, SEEK_SET); self::cache()->set($this->path, $contents); break; } return true; } /** * Stream metadata is not supported. * * @param $path * @param $option * @param $value * @return bool always false. */ public function stream_metadata($path, $option, $value) { return false; } /** * Opens a stream. * * @param $path * @param $mode * @param $options * @param $opened_path * @return bool true on success, false otherwise. * @throws RuntimeException */ public function stream_open($path, $mode, $options, &$opened_path) { $contents = self::cache()->get($path); $fileExists = (false !== $contents); switch ($mode) { case 'r': case 'rb': case 'r+': case 'rb+': if (! $fileExists) { return false; } $this->fd = fopen('php://memory', "wb+"); fwrite($this->fd, $contents); fseek($this->fd, 0, SEEK_SET); break; case 'w': case 'wb': case 'w+': case 'wb+': $this->fd = fopen('php://memory', "wb+"); break; case 'a': case 'ab': case 'a+': case 'ab+': $this->fd = fopen('php://memory', "wb+"); if ($fileExists) { fwrite($this->fd, $contents); fseek($this->fd, 0, SEEK_END); } break; default: return false; } $this->path = $path; $this->mode = $mode; return true; } /** * Reads from a stream. * * @return string string of bytes. */ public function stream_read($count) { return fread($this->fd, $count); } /** * Performs a seek operation on a stream. * * @param $offset * @param $whence * @return int */ public function stream_seek($offset, $whence) { return fseek($this->fd, $offset, $whence); } /** * Stream option setting is not supported. * * @return bool always false. */ public function stream_set_option($option, $arg1, $arg2) { return false; } /** * Returns a stream stat information. * * @return array stat information. */ public function stream_stat() { return $this->url_stat($this->path, 0); } /** * Returns the current position in a stream. * * @return int the position. */ public function stream_tell() { return ftell($this->fd); } /** * Returns the number of bytes written. */ public function stream_write($data) { return fwrite($this->fd, $data); } /** * Deletes a file. Called in response to unlink($filename). */ public function unlink($path) { if (false === self::cache()->get($path)) { return false; } self::cache()->delete($path); return true; } /** * Returns stat information for a given path. * * @param string $path * @param int $flags * @return array stat information. */ public function url_stat($path, $flags) { $now = time(); $stat = [ 'dev' => 0, // no specific device number 'ino' => 0, // no inode number 'mode' => 0, // inode protection mode 'nlink' => 1, // number of links 'uid' => 0, // no userid of owner 'gid' => 0, // no groupid of owner 'rdev' => 0, // no device type, not inode device 'size' => 0, // size in bytes 'atime' => $now, // time of last access (Unix timestamp) 'mtime' => $now, // time of last modification (Unix timestamp) 'ctime' => $now, // time of last inode change (Unix timestamp) 'blksize' => 512, // blocksize of filesystem IO 'blocks' => 0, // number of 512-byte blocks allocated ** ]; if (array_has(self::$folders, $path)) { $stat['mode'] = self::DIR_MODE; return $stat; } $contents = self::cache()->get($path); if (false === $contents) { return false; } $size = strlen($contents); $stat['mode'] = self::FILE_MODE; $stat['size'] = $size; $stat['blocks'] = (int)(($size + 512) / 512); return $stat; } /** * Closes directory listing operation. * * @return bool always true. */ public function dir_closedir() { $this->dir = null; return true; } /** * Opens directory listing operation. * * @return bool always true. */ public function dir_opendir($path, $options) { $this->dir = $this->dir_list($path); return true; } /** * Returns the nex element in directory listing. * * @return string the next directory element name. */ public function dir_readdir() { return next($this->dir); } /** * Restarts directory listing operation. * * @return bool always true. */ public function dir_rewinddir() { reset($this->dir); return true; } /** * Creates a directory. * * @param string $path * @param int $mode * @param int $options * * @return boot true on success, false otherwise. */ public function mkdir($path, $mode, $options) { if (array_has(self::$folders, $path)) { return true; } $parentEnd = strrpos($path, '/'); $parent = substr($path, 0, $parentEnd); if (array_has(self::$folders, $parent)) { self::$folders[] = $path; return true; } if (! ($options & STREAM_MKDIR_RECURSIVE)) { return false; } while ($path !== self::PROTOCOL.':/') { if (array_has(self::$folders, $path)) { break; } else { self::$folders[] = $path; } $path = substr($path, 0, $parentEnd); $parentEnd = strrpos($path, '/'); } return true; } /** * Removes a directory. * * @param string $path * @param int $options * * @return boot true on success, false otherwise. */ public function rmdir($path, $options) { $dir = $this->dir_list($path); if (empty($dir)) { $count = count(self::$folders); for ($index = 0; index < $count; $index++) { if (self::$folders[$index] === $path) { unset(self::$folders[$index]); return true; } } } return false; } } ================================================ FILE: src/Shpasser/GaeSupportL5/Storage/Optimizer.php ================================================ basePath = $basePath; $this->runningInConsole = $runningInConsole; $this->initialized = false; $this->cachedFiles = array(); $this->configPath = self::CONFIG_PATH.'/config.php'; $this->routesPath = self::CONFIG_PATH.'/routes.php'; $this->servicesPath = self::CONFIG_PATH.'/services.json'; } /** * Bootstraps the Optimizer. * * @return boolean 'true' if successful, otherwise 'false'. */ public function bootstrap() { if (! $this->runningInConsole && $this->initializeFs()) { $this->buildFsTree(); $this->initialized = true; } return $this->initialized; } /** * Get the path to the configuration cache file. * * @return string */ public function getCachedConfigPath() { if ($this->initialized && env('CACHE_CONFIG_FILE')) { $this->cacheFile($this->basePath.'/bootstrap/cache/config.php', $this->configPath); return $this->configPath; } return false; } /** * Get the path to the routes cache file. * * @return string */ public function getCachedRoutesPath() { if ($this->initialized && env('CACHE_ROUTES_FILE')) { $this->cacheFile($this->basePath.'/bootstrap/cache/routes.php', $this->routesPath); return $this->routesPath; } return false; } /** * Get the path to the cached services.json file. * * @return string */ public function getCachedServicesPath() { return ($this->initialized && env('CACHE_SERVICES_FILE')) ? $this->servicesPath : false; } /** * Initializes the Cache Filesystem. */ protected function initializeFs() { return CacheFs::initialize(); } /** * Builds a filesystem tree in 'cachefs'. */ protected function buildFsTree() { mkdir(self::CONFIG_PATH, 0777, true); mkdir(self::COMPILED_VIEWS_PATH, 0777, true); } /** * Adds the requested file to cache. * * @param string $path path to the file to be cached. * @param string $cachefsPath path for the cached file(under 'cachefs://'). */ protected function cacheFile($path, $cachefsPath) { if (array_key_exists($path, $this->cachedFiles)) { return; } if (file_exists($path)) { $contents = file_get_contents($path); file_put_contents($cachefsPath, $contents); $this->cachedFiles[$path] = $cachefsPath; } } } ================================================ FILE: src/views/artisan.blade.php ================================================ Laravel Artisan Console for GAE

Artisan Console for Google App Engine

================================================ FILE: tests/.gitkeep ================================================ ================================================ FILE: tests/Shpasser/GaeSupportL5/Setup/ConfiguratorTest.php ================================================ open(__DIR__.'/resources.zip'); $zip->extractTo(__DIR__.'/playground'); // Call the configure() function on the 'testee' // to generate/modify the files. $configurator = new Configurator(new FakeCommand); $appId = 'laravel-app-gae-id'; $generateConfig = true; $cacheConfig = false; $bucketId = null; $dbSocket = '/cloudsql/test-app:test-cloud-sql'; $dbName = 'gae_test_db'; $dbHost = 'XXX.XXX.XXX.XXX'; $configurator->configure($appId, $generateConfig, $cacheConfig, $bucketId, $dbSocket, $dbName, $dbHost); } /** * Cleans up the 'playground' with all of its contents. */ protected function tearDown() { self::delTree(__DIR__.'/playground'); } public function testEnvProductionGeneration() { $env_production = __DIR__.'/playground/.env.production'; $expected = __DIR__.'/playground/.env.production_expected_result'; $this->assertFileEquals($env_production, $expected); } public function testEnvLocalGeneration() { $env_local = __DIR__.'/playground/.env.local'; $expected = __DIR__.'/playground/.env.local_expected_result'; $this->assertFileEquals($env_local, $expected); } public function testBootstrapAppModification() { $bootstrap_app_php = __DIR__.'/playground/bootstrap/app.php'; $expected = __DIR__.'/playground/bootstrap/app.php_expected_result'; $this->assertFileEquals($bootstrap_app_php, $expected); } public function testConfigAppModification() { $config_app_php = __DIR__.'/playground/config/app.php'; $expected = __DIR__.'/playground/config/app.php_expected_result'; $this->assertFileEquals($config_app_php, $expected); } public function testConfigViewModification() { $config_view_php = __DIR__.'/playground/config/view.php'; $expected = __DIR__.'/playground/config/view.php_expected_result'; $this->assertFileEquals($config_view_php, $expected); } public function testConfigQueueModification() { $config_queue_php = __DIR__.'/playground/config/queue.php'; $expected = __DIR__.'/playground/config/queue.php_expected_result'; $this->assertFileEquals($config_queue_php, $expected); } public function testConfigDatabaseModification() { $config_queue_php = __DIR__.'/playground/config/database.php'; $expected = __DIR__.'/playground/config/database.php_expected_result'; $this->assertFileEquals($config_queue_php, $expected); } public function testGenerateAppYaml() { $app_yaml = __DIR__.'/playground/app.yaml'; $expected = __DIR__.'/playground/app.yaml_expected_result'; $this->assertFileEquals($app_yaml, $expected); } public function testGeneratePhpIni() { $php_ini = __DIR__.'/playground/php.ini'; $expected = __DIR__.'/playground/php.ini_expected_result'; $this->assertFileEquals($php_ini, $expected); } } ================================================ FILE: tests/Shpasser/GaeSupportL5/Setup/FakeCommand.php ================================================