PHP Support

PHP is a popular scripting language designed especially for the web. It currently powers over 80% of websites.

Supported versions

  • 7.2
  • 7.3
  • 7.4

Note that as of PHP 7.1 we use the Zend Thread Safe (ZTS) version of PHP.

To specify a PHP container, use the type property in your .platform.app.yaml.

type: 'php:7.4'

Deprecated versions

The following versions are available but are not receiving security updates from upstream, so their use is not recommended. They will be removed at some point in the future.

  • 5.4
  • 5.5
  • 5.6
  • 7.0
  • 7.1

Support libraries

While it is possible to read the environment directly from your application, it is generally easier and more robust to use the platformsh/config-reader Composer library which handles decoding of service credential information for you.

Alternate start commands

PHP is most commonly run in a CGI mode, using PHP-FPM. That is the default on Platform.sh. However, you can also start alternative processes if desired, such as if you're running an Async PHP daemon, a thread-based worker process, etc. To do so, simply specify an alternative start command in platform.app.yaml, similar to the following:

web:
    commands:
        start: php run.php
    upstream:
            socket_family: tcp
            protocol: http

The above configuration will execute the run.php script in the application root when the container starts using the PHP-CLI SAPI, just before the deploy hook runs, but will not launch PHP-FPM. It will also tell the front-controller (Nginx) to connect to your application via a TCP socket, which will be specified in the PORT environment variable. Note that the start command must run in the foreground.

If not specified, the effective default start command varies by PHP version:

  • On PHP 5.x, it's /usr/sbin/php5-fpm.
  • On PHP 7.0, it's /usr/sbin/php-fpm7.0.
  • On PHP 7.1, it's /usr/sbin/php-fpm7.1-zts.
  • On PHP 7.2, it's /usr/sbin/php-fpm7.2-zts.
  • On PHP 7.3, it's /usr/sbin/php-fpm7.3-zts.
  • On PHP 7.4, it's /usr/sbin/php-fpm7.4-zts.

While you can call it manually that is generally not necessary. Note that PHP-FPM cannot run simultaneously along with another persistent process (such as ReactPHP or Amp). If you need both they will have to run in separate containers.

Expanded dependencies

In addition to the standard dependencies format, it is also possible to specify alternative repositories for use by Composer. The standard format like so:

dependencies:
    php:
        "platformsh/client": "dev-master"

is equivalent to composer require platform/client dev-master. However, you can also specify explicit require and repositories blocks:

dependencies:
    php:
        require:
            "platformsh/client": "dev-master"
        repositories:
            - type: vcs
              url: "git@github.com:platformsh/platformsh-client-php.git"

That would install platformsh/client from the alternate repository specified, as a global dependency. That is, it is equivalent to the following composer.json file:

{
    "repositories": [
        {
            "type": "vcs",
            "url":  "git@github.com:platformsh/platformsh-client-php.git"
        }
    ],
    "require": {
        "platformsh/client": "dev-master"
    }
}

That allows you to install a forked version of a global dependency from a custom repository.

Opcache preloading

PHP 7.4 introduced a new feature called Opcache Preloading, which allows you to load selected files into shared memory when PHP-FPM starts. That means functions and classes in those files are always available and do not need to be autoloaded, at the cost of any changes to those files requiring a PHP-FPM restart. Since PHP-FPM restarts anyway when a new deploy happens this feature is a major win on Platform.sh, and we recommend using it aggressively.

To enable preloading, add a php.ini value that specifies a preload script. Any php.ini mechanism will work, but using a variable in .platform.app.yaml is the recommended approach:

variables:
    php:
        opcache.preload: 'preload.php'

The opcache.preload value is evaluated as a file path relative to the application root (where .platform.app.yaml is), and it may be any PHP script that calls opcache_compile_file(). The following example will preload all .php files anywhere in the vendor directory:

$directory = new RecursiveDirectoryIterator(getenv('PLATFORM_APP_DIR') . '/vendor');
$iterator = new RecursiveIteratorIterator($directory);
$regex = new RegexIterator($iterator, '/^.+\.php$/i', RecursiveRegexIterator::GET_MATCH);

foreach ($regex as $key => $file) {
    // This is the important part!
    opcache_compile_file($file[0]);
}

Note: Preloading all .php files may not be optimal for your application, and may even introduce errors. Your application framework may provide recommendations or a pre-made presload script to use instead. Determining an optimal preloading strategy is the user's responsibility.

Debug PHP-FPM

If you want to inspect what's going on with PHP-FPM, you can install this small CLI:

dependencies:
  php:
    wizaplace/php-fpm-status-cli: "^1.0"

Then when you are connected to your project over SSH you can run:

$ php-fpm-status --socket=unix://$SOCKET --path=/-/status --full

Accessing services

To access various services with PHP, see the following examples. The individual service pages have more information on configuring each service.

Elasticsearch
Memcached
MongoDB
MySQL
PostgreSQL
RabbitMQ
Redis
Solr
<?php

declare(strict_types=1);

use Elasticsearch\ClientBuilder;
use Platformsh\ConfigReader\Config;

// Create a new config object to ease reading the Platform.sh environment variables.
// You can alternatively use getenv() yourself.
$config = new Config();

// Get the credentials to connect to the Elasticsearch service.
$credentials = $config->credentials('elasticsearch');

try {
    // The Elasticsearch library lets you connect to multiple hosts.
    // On Platform.sh Standard there is only a single host so just
    // register that.
    $hosts = [
        [
            'scheme' => $credentials['scheme'],
            'host' => $credentials['host'],
            'port' => $credentials['port'],
        ]
    ];

    // Create an Elasticsearch client object.
    $builder = ClientBuilder::create();
    $builder->setHosts($hosts);
    $client = $builder->build();

    $index = 'my_index';
    $type = 'People';

    // Index a few document.
    $params = [
        'index' => $index,
        'type' => $type,
    ];

    $names = ['Ada Lovelace', 'Alonzo Church', 'Barbara Liskov'];

    foreach ($names as $name) {
        $params['body']['name'] = $name;
        $client->index($params);
    }

    // Force just-added items to be indexed.
    $client->indices()->refresh(array('index' => $index));


    // Search for documents.
    $result = $client->search([
        'index' => $index,
        'type' => $type,
        'body' => [
            'query' => [
                'match' => [
                    'name' => 'Barbara Liskov',
                ],
            ],
        ],
    ]);

    if (isset($result['hits']['hits'])) {
        print <<<TABLE
<table>
<thead>
<tr><th>ID</th><th>Name</th></tr>
</thead>
<tbody>
TABLE;
        foreach ($result['hits']['hits'] as $record) {
            printf("<tr><td>%s</td><td>%s</td></tr>\n", $record['_id'], $record['_source']['name']);
        }
        print "</tbody>\n</table>\n";
    }

    // Delete documents.
    $params = [
        'index' => $index,
        'type' => $type,
    ];

    $ids = array_map(function($row) {
        return $row['_id'];
    }, $result['hits']['hits']);

    foreach ($ids as $id) {
        $params['id'] = $id;
        $client->delete($params);
    }

} catch (Exception $e) {
    print $e->getMessage();
}
<?php

declare(strict_types=1);

use Platformsh\ConfigReader\Config;

// Create a new config object to ease reading the Platform.sh environment variables.
// You can alternatively use getenv() yourself.
$config = new Config();

// Get the credentials to connect to the Memcached service.
$credentials = $config->credentials('memcached');

try {
    // Connecting to Memcached server.
    $memcached = new Memcached();
    $memcached->addServer($credentials['host'], $credentials['port']);
    $memcached->setOption(Memcached::OPT_BINARY_PROTOCOL, true);

    $key = "Deploy day";
    $value = "Friday";

    // Set a value.
    $memcached->set($key, $value);

    // Read it back.
    $test = $memcached->get($key);

    printf('Found value <strong>%s</strong> for key <strong>%s</strong>.', $test, $key);

} catch (Exception $e) {
    print $e->getMessage();
}
<?php

declare(strict_types=1);

use Platformsh\ConfigReader\Config;
use MongoDB\Client;

// Create a new config object to ease reading the Platform.sh environment variables.
// You can alternatively use getenv() yourself.
$config = new Config();

// The 'database' relationship is generally the name of primary database of an application.
// It could be anything, though, as in the case here here where it's called "mongodb".
$credentials = $config->credentials('mongodb');

try {

    $server = sprintf('%s://%s:%s@%s:%d/%s',
        $credentials['scheme'],
        $credentials['username'],
        $credentials['password'],
        $credentials['host'],
        $credentials['port'],
        $credentials['path']
    );

    $client = new Client($server);
    $collection = $client->main->starwars;

    $result = $collection->insertOne([
        'name' => 'Rey',
        'occupation' => 'Jedi',
    ]);

    $id = $result->getInsertedId();

    $document = $collection->findOne([
        '_id' => $id,
    ]);

    // Clean up after ourselves.
    $collection->drop();

    printf("Found %s (%s)<br />\n", $document->name, $document->occupation);

} catch (\Exception $e) {
    print $e->getMessage();
}
<?php

declare(strict_types=1);

use Platformsh\ConfigReader\Config;

// Create a new config object to ease reading the Platform.sh environment variables.
// You can alternatively use getenv() yourself.
$config = new Config();

// The 'database' relationship is generally the name of primary SQL database of an application.
// That's not required, but much of our default automation code assumes it.
$credentials = $config->credentials('database');

try {
    // Connect to the database using PDO.  If using some other abstraction layer you would
    // inject the values from $database into whatever your abstraction layer asks for.
    $dsn = sprintf('mysql:host=%s;port=%d;dbname=%s', $credentials['host'], $credentials['port'], $credentials['path']);
    $conn = new \PDO($dsn, $credentials['username'], $credentials['password'], [
        // Always use Exception error mode with PDO, as it's more reliable.
        \PDO::ATTR_ERRMODE => \PDO::ERRMODE_EXCEPTION,
        // So we don't have to mess around with cursors and unbuffered queries by default.
        \PDO::MYSQL_ATTR_USE_BUFFERED_QUERY => TRUE,
        // Make sure MySQL returns all matched rows on update queries including
        // rows that actually didn't have to be updated because the values didn't
        // change. This matches common behavior among other database systems.
        \PDO::MYSQL_ATTR_FOUND_ROWS => TRUE,
    ]);

    // Creating a table.
    $sql = "CREATE TABLE People (
      id INT(6) UNSIGNED AUTO_INCREMENT PRIMARY KEY,
      name VARCHAR(30) NOT NULL,
      city VARCHAR(30) NOT NULL
      )";
    $conn->query($sql);

    // Insert data.
    $sql = "INSERT INTO People (name, city) VALUES 
        ('Neil Armstrong', 'Moon'), 
        ('Buzz Aldrin', 'Glen Ridge'), 
        ('Sally Ride', 'La Jolla');";
    $conn->query($sql);

    // Show table.
    $sql = "SELECT * FROM People";
    $result = $conn->query($sql);
    $result->setFetchMode(\PDO::FETCH_OBJ);

    if ($result) {
        print <<<TABLE
<table>
<thead>
<tr><th>Name</th><th>City</th></tr>
</thead>
<tbody>
TABLE;
        foreach ($result as $record) {
            printf("<tr><td>%s</td><td>%s</td></tr>\n", $record->name, $record->city);
        }
        print "</tbody>\n</table>\n";
    }

    // Drop table
    $sql = "DROP TABLE People";
    $conn->query($sql);

} catch (\Exception $e) {
    print $e->getMessage();
}
<?php

declare(strict_types=1);

use Platformsh\ConfigReader\Config;

// Create a new config object to ease reading the Platform.sh environment variables.
// You can alternatively use getenv() yourself.
$config = new Config();

// The 'database' relationship is generally the name of primary SQL database of an application.
// It could be anything, though, as in the case here here where it's called "postgresql".
$credentials = $config->credentials('postgresql');

try {
    // Connect to the database using PDO.  If using some other abstraction layer you would
    // inject the values from $database into whatever your abstraction layer asks for.
    $dsn = sprintf('pgsql:host=%s;port=%d;dbname=%s', $credentials['host'], $credentials['port'], $credentials['path']);
    $conn = new \PDO($dsn, $credentials['username'], $credentials['password'], [
        // Always use Exception error mode with PDO, as it's more reliable.
        \PDO::ATTR_ERRMODE => \PDO::ERRMODE_EXCEPTION,
        // So we don't have to mess around with cursors and unbuffered queries by default.
    ]);

    $conn->query("DROP TABLE IF EXISTS People");

    // Creating a table.
    $sql = "CREATE TABLE IF NOT EXISTS People (
      id SERIAL PRIMARY KEY,
      name VARCHAR(30) NOT NULL,
      city VARCHAR(30) NOT NULL
      )";
    $conn->query($sql);

    // Insert data.
    $sql = "INSERT INTO People (name, city) VALUES
        ('Neil Armstrong', 'Moon'),
        ('Buzz Aldrin', 'Glen Ridge'),
        ('Sally Ride', 'La Jolla');";
    $conn->query($sql);

    // Show table.
    $sql = "SELECT * FROM People";
    $result = $conn->query($sql);
    $result->setFetchMode(\PDO::FETCH_OBJ);

    if ($result) {
        print <<<TABLE
<table>
<thead>
<tr><th>Name</th><th>City</th></tr>
</thead>
<tbody>
TABLE;
        foreach ($result as $record) {
            printf("<tr><td>%s</td><td>%s</td></tr>\n", $record->name, $record->city);
        }
        print "</tbody>\n</table>\n";
    }

    // Drop table.
    $sql = "DROP TABLE People";
    $conn->query($sql);

} catch (\Exception $e) {
    print $e->getMessage();
}
<?php

declare(strict_types=1);

use Platformsh\ConfigReader\Config;
use PhpAmqpLib\Connection\AMQPStreamConnection;
use PhpAmqpLib\Message\AMQPMessage;

// Create a new config object to ease reading the Platform.sh environment variables.
// You can alternatively use getenv() yourself.
$config = new Config();

// Get the credentials to connect to the RabbitMQ service.
$credentials = $config->credentials('rabbitmq');

try {

    $queueName = 'deploy_days';

    // Connect to the RabbitMQ server.
    $connection = new AMQPStreamConnection($credentials['host'], $credentials['port'], $credentials['username'], $credentials['password']);
    $channel = $connection->channel();

    $channel->queue_declare($queueName, false, false, false, false);

    $msg = new AMQPMessage('Friday');
    $channel->basic_publish($msg, '', 'hello');

    echo "[x] Sent 'Friday'<br/>\n";

    // In a real application you't put the following in a separate script in a loop.
    $callback = function ($msg) {
        printf("[x] Deploying on %s<br />\n", $msg->body);
    };

    $channel->basic_consume($queueName, '', false, true, false, false, $callback);

    // This blocks on waiting for an item from the queue, so comment it out in this demo script.
    //$channel->wait();

    $channel->close();
    $connection->close();

} catch (Exception $e) {
    print $e->getMessage();
}
<?php

declare(strict_types=1);

use Platformsh\ConfigReader\Config;

// Create a new config object to ease reading the Platform.sh environment variables.
// You can alternatively use getenv() yourself.
$config = new Config();

// Get the credentials to connect to the Redis service.
$credentials = $config->credentials('redis');

try {
    // Connecting to Redis server.
    $redis = new Redis();
    $redis->connect($credentials['host'], $credentials['port']);

    $key = "Deploy day";
    $value = "Friday";

    // Set a value.
    $redis->set($key, $value);

    // Read it back.
    $test = $redis->get($key);

    printf('Found value <strong>%s</strong> for key <strong>%s</strong>.', $test, $key);

} catch (Exception $e) {
    print $e->getMessage();
}
<?php
declare(strict_types=1);

use Platformsh\ConfigReader\Config;
use Solarium\Client;

// Create a new config object to ease reading the Platform.sh environment variables.
// You can alternatively use getenv() yourself.
$config = new Config();

// Get the credentials to connect to the Solr service.
$credentials = $config->credentials('solr');

try {

    $config = [
        'endpoint' => [
            'localhost' => [
                'host' => $credentials['host'],
                'port' => $credentials['port'],
                'path' => "/" . $credentials['path'],
            ]
        ]
    ];

    $client = new Client($config);

    // Add a document
    $update = $client->createUpdate();

    $doc1 = $update->createDocument();
    $doc1->id = 123;
    $doc1->name = 'Valentina Tereshkova';

    $update->addDocuments(array($doc1));
    $update->addCommit();

    $result = $client->update($update);
    print "Adding one document. Status (0 is success): " .$result->getStatus(). "<br />\n";

    // Select one document
    $query = $client->createQuery($client::QUERY_SELECT);
    $resultset = $client->execute($query);
    print  "Selecting documents (1 expected): " .$resultset->getNumFound() . "<br />\n";

    // Delete one document
    $update = $client->createUpdate();

    $update->addDeleteById(123);
    $update->addCommit();
    $result = $client->update($update);
    print "Deleting one document. Status (0 is success): " .$result->getStatus(). "<br />\n";

} catch (Exception $e) {
    print $e->getMessage();
}

Runtime configuration

It is possible to change the PHP-FPM runtime configuration via the runtime block on your .platform.app.yaml. The PHP-FPM options below are configurable:

  • request_terminate_timeout - The timeout for serving a single request after which the PHP-FPM worker process will be killed. That is separate from the PHP runtime's max_execution_time ini option, which is preferred. This option may be used if the PHP process is dying without cleaning up properly and causing the FPM process to hang.
runtime:
    request_terminate_timeout: 300

Project templates

A number of project templates for major PHP applications are available on GitHub. Not all of them are proactively maintained but all can be used as a starting point or reference for building your own website or web application.

Applications

Examples

Frameworks