WordPress as a Development Platform

Many people dislike WordPress code. It’s no secret that for contemporary PHP developers WordPress feels antiquated. The founder of WordPress was even once-upon-a-time vocal about not keeping up to date with the PHP eco-system because reasons.

Times changed. So did PHP. So did WordPress.

To give credit where credit is due, the reasoning behind WordPress’ conservative change management is sound. They don’t want to mess with their insanely huge user base.

WordPress powers 27.8% of all websites on the internet.

That’s a lot of users. By choosing WordPress as your development platform you get massive traction for free.

But… that code. Ugh!

The good news is that when you develop for WordPress you don’t ever touch WordPress code. Instead you write a Plugin. [1] I put forward that in 2017 nothing is stopping you from writing a good, clean Plugin other than yourself.

Environment

WordPress is PHP 7 compatible. WordPress is also HHVM compatible. Running on either vastly increases performance.

It follows that if your environment is PHP 7 then you get the syntax.

Syntax

WordPress Plugins can have PSR compatible namespaces.

WordPress’ answer to Event Dispatcher (and/or Observer) are the add_action() and add_filter() functions. These functions are compatible with closures.

Meaning you can write code like:

add_action('init', function() use ($v) {
    (new \Acme\Foo\Bar\SomeClass($v))->someMethod();
});

add_action('init', '\Acme\some_function');

add_action('init', ['\Acme\Foo\Bar\SomeOtherClass', 'someStaticMethod']);

add_action('init', [$this, 'someOtherMethod']);

Or *any* standards compliant PHP 7 code you want to write.

Tools

PHPStorm supports WordPress Plugin development out-of-the-box.

WP-CLI is a set of command-line tools for managing WordPress installations. It simplifies many developer and deployment related tasks and makes unit testing your plugin possible.

WordPress Plugins have PHP CodeSniffer rules ready to go.

WordPress Plugins can be installed using Composer.

Bedrock and Trellis for the win.

Open

WordPress is licenced under the GPL. This still matters.

Getting Things Done

Is there room for improvement? Of course! Just like PHP, WordPress is always improving with the caveat that just like PHP, WordPress strives to keep backwards compatibly.

Developers rejoice, WordPress is moving forward, kicking and screaming as we drag it into the future.

[1] WordPress also has a REST API if you’re into that kind of thing…

Using MySQL Deadlocks To Avoid Overselling

When developing an e-commerce application, unless you work at United Airlines, you generally want to avoid overselling.

Instead of punching your customers in the face why not use MySQL Deadlocks? (Turns out this is a feature not a bug!)

First attempt, creating deadlocks

MySQL has 4 transaction isolation levels: SERIALIZABLE, REPEATABLE READ, READ UNCOMMITTED, READ COMMITTED.

In the following proof of concept, where we have 50 of the same product in stock, and we run seige to represent concurrent customers buying the same product at the same time, we expect 50 “Success!” messages in our log files.

When we use any of REPEATABLE READ, READ UNCOMMITTED, or READ COMMITTED we oversell. (boo!)

When we use SERIALIZABLE we do not oversell (yay!) but some users get deadlock errors while others do not. (SQLSTATE[40001]: Serialization failure: 1213 Deadlock found when trying to get lock; try restarting transaction)

<?php

error_reporting(E_ALL | E_STRICT); // Development

/*
SQL:
CREATE DATABASE `deadlocktest` COLLATE 'utf8_general_ci';
CREATE TABLE `products` ( `id` int NOT NULL AUTO_INCREMENT PRIMARY KEY, `inventory` int NOT NULL );
INSERT INTO `products` (`id`, `inventory`) VALUES ('123', '50');

USEFUL LINUX COMMANDS:
$ rm log.txt; touch log.txt; chmod 777 log.txt
$ seige http://host/file.php
*/

// ------------------------------------------------------------------
// Config
// ------------------------------------------------------------------

$mysqlIsolation = 'SERIALIZABLE'; // ( SERIALIZABLE, REPEATABLE READ, READ UNCOMMITTED, READ COMMITTED )
$productId = 123;
$logFile = __DIR__ . '/log.txt';

$host = '127.0.0.1';
$db = 'deadlocktest';
$user = 'root';
$pass = '';
$charset = 'utf8';
$opt = [
    PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
    PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
    PDO::ATTR_EMULATE_PREPARES => false,
];

// ------------------------------------------------------------------
// Functions
// ------------------------------------------------------------------

/**
 * Simulate time it takes to call the payment gateway and do stuff
 */
function doPaymentGatewayStuff()
{
    usleep(500000); // Wait for 0.5 seconds
}

/**
 * Simulate buying a product from our inventory
 *
 * @param PDO $pdo
 * @param int $productId
 * @return int
 * @throws Exception
 */
function buyProduct(PDO $pdo, int $productId): int
{
    $pdo->beginTransaction();

    $selectStmt = $pdo->prepare('SELECT inventory FROM products WHERE id = :id ');
    $selectStmt->execute(['id' => $productId]);
    $res = $selectStmt->fetch();
    if ($res['inventory'] <= 0) {
        throw new Exception("Oh no! Sorry we're out inventory!");
    }

    $newInventory = $res['inventory'] - 1;
    $updateStmt = $pdo->prepare('UPDATE products SET inventory = :inventory WHERE id = :id ');
    $updateStmt->execute(['inventory' => $newInventory, 'id' => $productId]);

    doPaymentGatewayStuff();

    $pdo->commit();

    return $newInventory;
}

// ------------------------------------------------------------------
// Procedure
// ------------------------------------------------------------------

$uniqueUser = uniqid();
try {
    // Set up DB driver
    $pdo = new PDO("mysql:host={$host};dbname={$db};charset={$charset}", $user, $pass, $opt);
    $pdo->query("SET TRANSACTION ISOLATION LEVEL {$mysqlIsolation} ");

    // Simulate buying a product and decreasing inventory
    $newInventory = buyProduct($pdo, $productId);

    // No exceptions were thrown, we consider this successful
    $successMsg = "{$uniqueUser} - Success! Product {$productId} inventory has been decreased to {$newInventory}" . PHP_EOL;
    file_put_contents($logFile, $successMsg, FILE_APPEND);
    echo "$successMsg";
}
catch (Exception $e) {
    if (isset($pdo) && $pdo->inTransaction()) {
        $pdo->rollBack();
    }
    $errorMsg = "{$uniqueUser} - Error! " . $e->getMessage() . PHP_EOL;
    file_put_contents($logFile, $errorMsg, FILE_APPEND);
    echo "$errorMsg";
}

Second attempt, handling deadlocks

The above code has good intentions but many users get the dreaded deadlock message.

Turns out deadlocks are OK! You just have to handle them somehow.

Here’s a fixed proof of concept:

<?php

// ------------------------------------------------------------------
// Config
// ------------------------------------------------------------------

$mysqlIsolation = 'SERIALIZABLE';
$productId = 123;
$logFile = __DIR__ . '/log.txt';

$host = '127.0.0.1';
$db = 'deadlocktest';
$user = 'root';
$pass = '';
$charset = 'utf8';
$opt = [
    PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
    PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
    PDO::ATTR_EMULATE_PREPARES => false,
];

// ------------------------------------------------------------------
// Functions
// ------------------------------------------------------------------

/**
 * Check if $e is of type MySQL deadlock
 *
 * @param PDO $pdo
 * @param mixed $e
 * @return bool
 */
function isDeadlock(PDO $pdo, $e): bool
{
    return (
        $e instanceof PDOException &&
        $pdo->getAttribute(PDO::ATTR_DRIVER_NAME) == 'mysql' &&
        $e->errorInfo[0] == 40001 &&
        $e->errorInfo[1] == 1213
    );
}

/**
 * Simulate time it takes to call the payment gateway and do stuff
 */
function doPaymentGatewayStuff()
{
    usleep(500000); // Wait for 0.5 seconds
}

/**
 * Simulate buying a product from our inventory
 *
 * @param PDO $pdo
 * @param int $productId
 * @return int
 * @throws Exception
 */
function buyProduct(PDO $pdo, int $productId): int
{
    $pdo->beginTransaction();

    $selectStmt = $pdo->prepare('SELECT inventory FROM products WHERE id = :id ');
    $selectStmt->execute(['id' => $productId]);
    $res = $selectStmt->fetch();
    if ($res['inventory'] <= 0) {
        throw new Exception("Oh no! Sorry we're out inventory!");
    }

    $newInventory = $res['inventory'] - 1;
    $updateStmt = $pdo->prepare('UPDATE products SET inventory = :inventory WHERE id = :id ');
    $updateStmt->execute(['inventory' => $newInventory, 'id' => $productId]);

    doPaymentGatewayStuff();

    $pdo->commit();

    return $newInventory;
}

// ------------------------------------------------------------------
// Procedure
// ------------------------------------------------------------------

$uniqueUser = uniqid();
$retry = true;
while ($retry)
{
    try {
        // Set up DB driver
        $pdo = new PDO("mysql:host={$host};dbname={$db};charset={$charset}", $user, $pass, $opt);
        $pdo->query("SET TRANSACTION ISOLATION LEVEL {$mysqlIsolation} ");

        // Simulate buying a product and decreasing inventory
        $newInventory = buyProduct($pdo, $productId);

        // No exceptions were thrown, we consider this successful
        $successMsg = "{$uniqueUser} - Success! Product {$productId} inventory has been decreased to {$newInventory}" . PHP_EOL;
        file_put_contents($logFile, $successMsg, FILE_APPEND);
        echo "$successMsg";
        $retry = false;
    }
    catch (Exception $e) {
        if (isset($pdo) && isDeadlock($pdo, $e)) {
            $retry = true;
        } else {
            $retry = false;
            if (isset($pdo) && $pdo->inTransaction()) {
                $pdo->rollBack();
            }
            $errorMsg = "{$uniqueUser} - Error! " . $e->getMessage() . PHP_EOL;
            file_put_contents($logFile, $errorMsg, FILE_APPEND);
            echo "$errorMsg";
        }
    }
}

Huge gaping caveat: With 15 concurrent users the 15th user would be waiting for a long time. Patches welcome.

PHP Composer for Developers

Ever wanted to make a bugfix to a Composer package? You can!

Get a local git clone of the dependency by requiring it with the –prefer-source option.

composer require kizu514/package --prefer-source

But wait that’s not all! If you have your own GitHub namespace you can set things up so that your own code is always installed from source. For example, In the following composer.json snippet all the packages from kizu514 are installed from source, and everything else is dist.

{
    "config": {
        "preferred-install": {
            "kizu514/*": "source",
            "*": "dist"
        }
    }
}

Ever wanted to use a git branch instead of a specific version? You can!

Use inline aliases. To declare an inline alias you must:

  • Prefix branch names with: dev
  • No wildcards (*), must be unambigous.

For example, if my composer.json file had this in it:

"kizu514/package": "1.*",

Then to use a branch I would simply change it to:

"kizu514/package": "dev-BRANCH_NAME as 1.0.9",

Where BRANCH_NAME is a branch that exists on GitHub and 1.0.9 is unambiguous. If you want to check out a branch instead of a tag then simply do:

"kizu514/package": "dev-BRANCH_NAME",

What about private repos?

Use Private Packagist or add to your repositories configuration:

{
  "type": "vcs",
  "no-api": true,
  "url":  "git@github.com:kizu514/secret-project.git"
}

But wait that’s not all! Oh wait, yes, it is.

Manifest.json

My wife’s Japanese comic about our family is a responsive website.

A cool trick I learned at ConFoo while listening to Christian Heilmann speak was that I could leverage built-in mobile technology by simply adding a manifest.json file to the code.

A manifest turns a responsive website into an installable app. It lets users add it on their mobile phone’s home screen. When they launch the site it gets a splash screen and runs in full screen mode, basically behaving like a native app.

Caveat: For this to work HTTPS is required. Use certbot if you don’t already.

I used Manifest Generator to get started and it was easy. According to the ConFoo talk Bing indexes sites with manifest.json files and prioritizes them as smartphone compatible. A simple SEO win?

Now my family’s manga is an app. Horray for the open web!

Building a Simple API using Opulence PHP

This tutorial will show you how to code a simple JSON API using Opulence PHP. We will install Opulence’s skeleton project using composer, then create a ‘user’ database entity, and finally we will match CRUD (Create, Read, Update, Delete) to POST, GET, PUT, and DELETE.

Prerequisites: PHP7, Composer, MySQL.

Installing

Create an Opulence project with the following command:

composer create-project opulence/project SimpleApi --prefer-dist 

The default Opulence app name is Project. Using apex, rename it to SimpleApi.

cd SimpleApi
php apex app:rename Project SimpleApi

This command will output:

JSON Config

According to the documentation if a client does not request JSON then HTML will be returned. This is “the right way to do it” but for the sake of this API we always want to return JSON. We can do this by adding the following code to config/http/views.php:

if (!isset($_SERVER['CONTENT_TYPE'])) {
    $_SERVER['CONTENT_TYPE'] = 'application/json';
}

We also want to disable the default Session and Csrf middlewares because REST clients do not (always) work with cookies. Open config/http/middleware.php and comment out:

return [
    CheckMaintenanceMode::class,
//    Session::class,
//    CheckCsrfToken::class
];

Database Config

Out of the box PostgreSQL is the default database driver. To use MySQL change line ~5 in src/SimpleApi/Application/Bootstrappers/Databases/SqlBootstrapper.php from PostgreSQL to:

use Opulence\Databases\Adapters\Pdo\MySql\Driver;

Manually create a MySQL database named simpleapi and modify config/environment/.env.app.php accordingly.

Environment::setVar('DB_HOST', 'localhost');
Environment::setVar('DB_USER', 'root');
Environment::setVar('DB_PASSWORD', 'root');
Environment::setVar('DB_NAME', 'simpleapi');
Environment::setVar('DB_PORT', 3306);

Database Entity

Create a user table with the following columns: [ id (primary), email (unique), firstname (string), lastname (string), age (integer, optional) , country (2 character string) ]

CREATE TABLE `user` (
  `id` int NOT NULL AUTO_INCREMENT PRIMARY KEY,
  `email` varchar(255) NOT NULL UNIQUE,
  `firstname` varchar(255) NOT NULL,
  `lastname` varchar(255) NOT NULL,
  `age` int,
  `country` char(2) NOT NULL
);

Using apex, create a matching entity class named User.

php apex make:entity User

Note: These commands create stubs / empty templates. You must finish the code yourself!

Open the newly created src/SimpleApi/User.php and finish the mutator methods so that the properties match the database table. Implement JsonSerializable too.

<?php
namespace SimpleApi;

use Opulence\Orm\IEntity;

class User implements IEntity, \JsonSerializable
{
    /** @var int */
    private $id;

    /** @var string */
    private $email;

    /** @var string */
    private $firstname;

    /** @var string */
    private $lastname;

    /** @var int|null */
    private $age;

    /** @var string */
    private $country;

    public function getId(): int
    {
        return (int)$this->id;
    }

    public function setId($id): self
    {
        $this->id = $id;

        return $this;
    }

    public function getEmail(): string
    {
        return $this->email;
    }

    public function setEmail($email): self
    {
        $this->email = $email;

        return $this;
    }

    public function getFirstname(): string
    {
        return $this->firstname;
    }

    public function setFirstname($firstname): self
    {
        $this->firstname = $firstname;

        return $this;
    }

    public function getLastname(): string
    {
        return $this->lastname;
    }

    public function setLastname($lastname): self
    {
        $this->lastname = $lastname;

        return $this;
    }

    /**
     * @return int|null
     */
    public function getAge()
    {
        return $this->age;
    }

    public function setAge($age): self
    {
        $this->age = $age;

        return $this;
    }

    public function getCountry(): string
    {
        return $this->country;
    }

    public function setCountry($country): self
    {
        $this->country = $country;

        return $this;
    }

    public function jsonSerialize() : array
    {
        return [
            'id' => (int)$this->getId(),
            'email' => $this->getEmail(),
            'firstname' => $this->getFirstname(),
            'lastname' => $this->getLastname(),
            'age' => is_null($this->getAge()) ? null : (int)$this->getAge(),
            'country' => $this->getCountry(),
        ];
    }
}

Open src/SimpleApi/Application/Bootstrappers/Orm/OrmBootstrapper.php and register an ID generator for \SimpleApi\User

private function registerIdGenerators(IIdGeneratorRegistry $idGeneratorRegistry)
{
    // Register your Id generators for classes that will be managed by the unit of work
    $idGeneratorRegistry->registerIdGenerator(
        \SimpleApi\User::class,
        new \Opulence\Orm\Ids\Generators\IntSequenceIdGenerator('user_id_seq')
    );
}

Database Mapper

Using apex, create a SQL data mapper named User. When prompted pick SQL data mapper and use \SimpleApi\User as the entity.

php apex make:datamapper User

This command will output:

Open the newly created src/SimpleApi/Infrastructure/Orm/User.php and finish the stubs.

<?php
namespace SimpleApi\Infrastructure\Orm;

use Opulence\Orm\DataMappers\SqlDataMapper;
use Opulence\Orm\OrmException;

class User extends SqlDataMapper
{
    /**
     * Adds an entity to the database
     *
     * @param \SimpleApi\User $user The entity to add
     * @throws OrmException Thrown if the entity couldn't be added
     */
    public function add($user)
    {
        $statement = $this->writeConnection->prepare(
            'INSERT INTO user (email, firstname, lastname, age, country)
             VALUES (:email, :firstname, :lastname, :age, :country)'
        );
        $statement->bindValues([
            'email' => $user->getEmail(),
            'firstname' => $user->getFirstname(),
            'lastname' => $user->getLastname(),
            'age' => $user->getAge(),
            'country' => $user->getCountry(),
        ]);
        $statement->execute();
    }

    /**
     * Deletes an entity
     *
     * @param \SimpleApi\User $user The entity to delete
     * @throws OrmException Thrown if the entity couldn't be deleted
     */
    public function delete($user)
    {
        $statement = $this->writeConnection->prepare('DELETE FROM user WHERE id = :id');
        $statement->bindValues([
            'id' => [$user->getId(), \PDO::PARAM_INT]
        ]);
        $statement->execute();
    }

    /**
     * Gets all the entities
     *
     * @return \SimpleApi\User[] The list of all the entities
     */
    public function getAll() : array
    {
        $sql = 'SELECT * FROM user';

        return $this->read($sql, [], self::VALUE_TYPE_ARRAY);
    }

    /**
     * Gets the entity with the input Id
     *
     * @param int|string $id The Id of the entity we're searching for
     * @return \SimpleApi\User The entity with the input Id
     * @throws OrmException Thrown if there was no entity with the input Id
     */
    public function getById($id): \SimpleApi\User
    {
        $sql = 'SELECT * FROM user WHERE id = :id';
        $parameters = [
            'id' => [$id, \PDO::PARAM_INT]
        ];

        return $this->read($sql, $parameters, self::VALUE_TYPE_ENTITY, true);
    }

    /**
     * Saves any changes made to an entity
     *
     * @param \SimpleApi\User $user The entity to save
     * @throws OrmException Thrown if the entity couldn't be saved
     */
    public function update($user)
    {
        $statement = $this->writeConnection->prepare(
            'UPDATE user SET email = :email, firstname = :firstname, lastname = :lastname,
             age = :age, country = :country
             WHERE id = :id'
        );
        $statement->bindValues([
            'email' => $user->getEmail(),
            'firstname' => $user->getFirstname(),
            'lastname' => $user->getLastname(),
            'age' => $user->getAge(),
            'country' => $user->getCountry(),
            'id' => [$user->getId(), \PDO::PARAM_INT]
        ]);
        $statement->execute();
    }

    /**
     * Loads an entity from a hash of data
     *
     * @param array $hash The hash of data to load the entity from
     * @return \SimpleApi\User The entity
     */
    protected function loadEntity(array $hash): \SimpleApi\User
    {
        $entity = new \SimpleApi\User();

        $entity->setId($hash['id']);
        $entity->setEmail($hash['email']);
        $entity->setFirstname($hash['firstname']);
        $entity->setLastname($hash['lastname']);
        $entity->setAge($hash['age']);
        $entity->setCountry($hash['country']);

        return $entity;
    }
}

Controller Creation

Using apex, create a Controller named User. When prompted pick REST controller.

php apex make:controller User

This command will output:

Open the newly created src/SimpleApi/Application/Http/Controllers/User.php and finish the stubs. Type-hint any objects your controller needs in the controller’s constructor. Create a generic repository object for \SimpleApi\User.

<?php
namespace SimpleApi\Application\Http\Controllers;

use Opulence\Http\HttpException;
use Opulence\Http\Responses\JsonResponse;
use Opulence\Http\Responses\Response;
use Opulence\Orm\OrmException;
use Opulence\Orm\Repositories\Repository;
use Opulence\Orm\IUnitOfWork;
use Opulence\Routing\Controller;

class User extends Controller
{
    /** @var \Opulence\Orm\UnitOfWork */
    protected $unitOfWork;

    /** @var Repository */
    protected $repo;

    public function __construct(\SimpleApi\Infrastructure\Orm\User $dataMapper, IUnitOfWork $unitOfWork)
    {
        $this->unitOfWork = $unitOfWork;

        $this->repo = new Repository(
            \SimpleApi\User::class,
            $dataMapper,
            $this->unitOfWork
        );
    }

    /**
     * Creates a entity
     *
     * @return Response The response
     */
    public function create() : Response
    {
        $json = $this->request->getJsonBody();

        $user = new \SimpleApi\User();

        $user
            ->setEmail($json['email'])
            ->setFirstname($json['firstname'])
            ->setLastname($json['lastname'])
            ->setCountry($json['country']);

        if (isset($json['age'])) {
            $user->setAge($json['age']);
        }

        $this->repo->add($user);
        $this->unitOfWork->commit();

        return new JsonResponse($user);
    }

    /**
     * Deletes an entity
     *
     * @param mixed $id The Id of the entity
     * @return Response The response
     */
    public function delete($id) : Response
    {
        $user = $this->repo->getById($id);
        $this->repo->delete($user);
        $this->unitOfWork->commit();

        return new JsonResponse($user, 204);
    }

    /**
     * Shows an entity
     *
     * @param mixed $id The Id of the entity
     * @return Response The response
     * @throws HttpException
     */
    public function show($id) : Response
    {
        try {
            $user = $this->repo->getById($id);
        } catch (OrmException $e) {
            throw new HttpException(404);
        }

        return new JsonResponse($user);
    }

    /**
     * Shows all the entities
     *
     * @return Response The response
     */
    public function showAll() : Response
    {
        $user = $this->repo->getAll();

        return new JsonResponse($user);
    }

    /**
     * Updates an entity
     *
     * @param mixed $id The Id of the entity
     * @return Response The response
     */
    public function update($id) : Response
    {
        $json = $this->request->getJsonBody();

        /** @var \SimpleApi\User $user */
        $user = $this->repo->getById($id);

        if (isset($json['email'])) {
            $user->setEmail($json['email']);
        }
        if (isset($json['firstname'])) {
            $user->setFirstname($json['firstname']);
        }
        if (isset($json['lastname'])) {
            $user->setLastname($json['lastname']);
        }
        if (isset($json['country'])) {
            $user->setCountry($json['country']);
        }
        if (isset($json['age'])) {
            $user->setAge($json['age']);
        }

        $this->unitOfWork->commit();

        return new JsonResponse($user);
    }
}

Open config/http/routes.php and configure CRUD routes to use the controller class.

$router->group(['controllerNamespace' => 'SimpleApi\Application\Http\Controllers'], function (Router $router) {
    $router->group(['path' => '/user'], function (Router $router) {
        $router->get('', 'User@showAll');
        $router->post('', 'User@create');
    });
    $router->group(['path' => '/user/:id'], function (Router $router) {
        $router->get('', 'User@show');
        $router->put('', 'User@update');
        $router->delete('', 'User@delete');
    });
});

Barring any typos you should now have a simple API. To run Opulence locally, use the following command:

php apex app:runlocally

Use a REST client to POST the following JSON to the API:

POST: http://localhost/user

{
    "email": "foo@dev.null",
    "firstname": "Joe",
    "lastname": "Smith",
    "age": 999,
    "country": "JP"
}

Then try:

GET: http://localhost/user
GET: http://localhost/user/1
PUT: http://localhost/user/1
DELETE: http://localhost/user/1 

Got ideas on how to improve validation, error handling, security, or any other Opulence PHP tips? Post in the comments below.

Install PHP7 and Composer on Windows 10

PHP7 is a general purpose scripting language well suited for web development. Composer is the defacto package manager for PHP7. This tutorial will show you how to install PHP7 and Composer on Windows 10 for use in a command prompt.

A common misconception is that you need a web server like IIS, Apache, or Nginx to get started with PHP7 development. In fact, PHP7 has its own built in web server that you can invoke at the command prompt. Modern PHP frameworks such as Opulence, Symfony, Cake, Laravel, WordPress, and many more support this.

Installing PHP7

Download the latest PHP7 (non-thread safe version) zip file from http://windows.php.net/

Extract the contents of the zip file into C:\PHP7

Copy C:\PHP7\php.ini-development to C:\PHP7\php.ini

Open the newly copied C:\PHP7\php.ini in a text editor.

Scroll down to “Directory in which the loadable extensions (modules) reside.” and uncomment: extension_dir = “ext”

Notepad++ is great.

Scroll down to the DLL extensions section and uncomment the extensions you want to use.

My current setup.

Tweak other settings as needed.

Note: Don’t forget to keep your php.ini file in a safe place when you upgrade in the future!

Add C:\PHP7 to the Windows 10 system path environment variable.

Windows 10 has finally improved this interface, yay!

In a command prompt test that the installation is successful by typing php -v

ConEmu is great.

Installing Composer

On my computer I’ve created a C:\Users\dac514\bin directory where I keep miscellaneous executables. This directory is in my user path.

Use a search engine to find a tutorial and do something similar. Optionally install composer in the C:\PHP7 directory you just created as it’s already in your path.

To get composer.phar, drop to a command prompt, cd into your target directory, and run:

php -r "copy('https://getcomposer.org/installer', 'composer-setup.php');"
php composer-setup.php
php -r "unlink('composer-setup.php');"

(Important! Click here for a more secure and up-to-date install snippet.)

Next, create a new composer.bat file alongside the composer.phar file so that Windows 10 can execute it more easily. (Source)

echo @php "%~dp0composer.phar" %*>composer.bat

Test that it’s working by typing composer -V

ConEmu is still great.

How To Study And Pass The PSD-I Assessment From Scrum.org

The Scrum.org Professional Scrum Developer I assessment is a 60 minute time boxed test where you answer 80 multiple choice type questions. It costs $200 to take and the passing score is 85%.

I took the test September 14, 2016 and passed. My Score was 71 points (or 88.8%)

Scrum.org PSD-I Certificate

Here’s how I studied:

Install Recoll.

Create a directory  where you store any studied Scrum.org document. Setup an appropriate indexing strategy so that Recoll search is rebuilt every time you add a new document to your Scrum study folder.

Study the following documents:

Save the above documents into your study folder.

Read posts in the Scrum.org forum.

Once a day, for at least a few weeks, do the SCRUM OPEN, PRODUCT OWNER OPEN, and DEVELOPER OPEN assessments. When you finish an open assessment it will let you print a summary of your results. Save every summary in your study folder.

Keep doing the open assessments until you consistently get 100% every time

Keep doing the open assessments until you’ve seen every possible question.

The day before doing the test, take the time to create a single open assessments master document. Copy, paste (and study) all the information from all the open assessment summaries into the master document. When creating your master document avoid duplicate questions. Having to search through duplicates will slow you down.

Ignore any resource that doesn’t come from Scrum.org

When you’re ready, pay money to do the test, use your Recoll index to search for help when you don’t know an answer.

Good luck.

Spellcheck English + French At The Same Time On Android

On Android you can spellcheck multiple languages by switching to them, on-the-fly, using the space bar:

spellcheck-settings-04

To set up this feature go into settings under “Language & input” then select “Google Keyboard:”

spellcheck-settings-01

Select “Languages:”

spellcheck-settings-02

Disable “Use system language” and select the languages you want to spellcheck against:

spellcheck-settings-03

Merci!