StoreCore

Performance guidelines

Performance is one of the leading StoreCore™ design principles. This StoreCore developer guide contains several do’s and don’ts on PHP and MySQL performance.

This documentation is a work in progress. It describes prerelease software, and is subject to change. All code is released as free and open-source software (FOSS) under the GNU General Public License.

Do your own math

Recalculating a fixed value on the server is inefficient. Calculate the fixed value once yourself and use the result. Add a comment if you need to clarify the value.

thumb_down Incorrect:
setcookie('language', $lang, time() + 60 * 60 * 24 * 30, '/');
thumb_up Correct:
setcookie('language', $lang, time() + 2592000, '/');
thumb_up Recommended:
// Cookie expires in 60 seconds * 60 minutes * 24 hours * 30 days = 2592000 seconds
setcookie('language', $lang, time() + 2592000, '/');

Don’t use naive getters and setters

Getters and setters are methods that are used to get (read) and set (write) the value of an object's property. In object-oriented programming (OOP), getters and setters are often used to control access to private or protected properties.

A naive getter is a getter (or accessor) that simply returns the value of the property. A naive setter is a setter (or mutator) that simply sets the value of the property. Here is an example of a naive getter in PHP:

public function getName(): string
{
    return $this->name;
}

This getter simply returns the value of the $name class property. A similar naive setter for the same property would be something like this:

public function setName(string $name): void
{
    $this->name $name;
}

This setter is naive because it does not do any validation or checking to ensure that the value is valid. If strict typing is enforced in PHP, the method only allows string input, but this string may be empty.

In general, you should avoid using naive getters and naive setters in object-oriented PHP. They merely add code that basically does nothing.

thumb_down Not recommended:
class Person
{
    private string $firstName;
    private string $lastName;

    public function setFirstName(string $first_name): void
    {
        $this->firstName $first_name;
    }

    public function getFirstName(): string
    {
        return $this->firstName;
    }

    public function setLastName(string $last_name): void
    {
        $this->lastName $last_name;
    }
    
    public function getLastName(): string
    {
        return $this->lastName;
    }
}
thumb_up Recommended:
class Person
{
    public string $firstName;
    public string $lastName;
}

Here are some additional things to keep in mind when using getters and setters:

  • Getters and setters should be used to encapsulate private or protected properties.
  • Setters should be used to perform validation and other checks on the value of a property before it is set.
  • Getters should be used to return the value of a property in a consistent format.

Order database table columns for performance

In some databases, it is more efficient to order the columns in a specific manner because of the way the disk access is performed. The optimal order of columns in a MySQL or MariaDB table that uses the InnoDB storage engine is:

  • primary key
  • combined primary keys as defined in the KEY order
  • foreign keys used in JOIN queries
  • columns with an INDEX used in WHERE conditions or ORDER BY statements
  • others columns used in WHERE conditions
  • others columns used in ORDER BY statements
  • VARCHAR columns with a variable length
  • large TEXT and BLOB columns.

When there are many VARCHAR columns (with variable length) in a MySQL or MariaDB table, the column order MAY affect the performance of queries. The less close a column is to the beginning of the row, the more preceding columns the InnoDB storage engine should examine to find out the offset of a given one. Columns that are closer to the beginning of the table are therefore selected faster.

Store DateTimes as UTC timestamps

Times and dates with times SHOULD be stored in Coordinated Universal Time (UTC). The following examples illustrate this requirement with column definitions in a CREATE TABLE SQL statement.

thumb_down Incorrect:
`date_created`  DATETIME  NOT NULL
thumb_up Correct:
`date_created`  DATETIME  NOT NULL  DEFAULT CURRENT_TIMESTAMP
thumb_down Incorrect:
`date_modified`  DATETIME  NOT NULL
thumb_up Correct:
`date_modified`  DATETIME  NOT NULL  ON UPDATE CURRENT_TIMESTAMP

When there are two timestamps in the same database table, the logical thing to do is setting the creation date date_created to DEFAULT CURRENT_TIMESTAMP for the initial INSERT query and the modification date date_modified to ON UPDATE CURRENT_TIMESTAMP for all subsequent UPDATE queries:

thumb_down Not recommended:
`date_created`   DATETIME  NOT NULL  DEFAULT CURRENT_TIMESTAMP,
`date_modified`  DATETIME  NOT NULL  DEFAULT '0000-00-00 00:00:00'  ON UPDATE CURRENT_TIMESTAMP

This, however, only works in MySQL 5.6+. Older versions of MySQL will report an error: Incorrect table definition; there can be only one TIMESTAMP column with CURRENT_TIMESTAMP in DEFAULT or ON UPDATE clause.

The workaround currently implemented in Store­Core is to set the DEFAULT value for the initial INSERT timestamp to '0000-00-00 00:00:00' and only use the CURRENT_TIMESTAMP for a subsequent ON UPDATE:

thumb_up Recommended:
`date_created`   DATETIME  NOT NULL  DEFAULT '0000-00-00 00:00:00',
`date_modified`  DATETIME  NOT NULL  DEFAULT CURRENT_TIMESTAMP  ON UPDATE CURRENT_TIMESTAMP

Don’t cast database integers to SQL strings

String equality comparisons are much more expensive database operations than integer compares. If a database value is an integer, it MUST NOT be treated as a numeric string. This holds especially true for primary keys and foreign keys.

thumb_down Incorrect:
$sql = "
    UPDATE sc_addresses
       SET customer_id = '" . (int) $customer_id . "'
     WHERE address_id = '" . (int) $address_id . "'";
thumb_up Correct:
$sql = '
    UPDATE sc_addresses
       SET customer_id = ' . (int) $customer_id . '
     WHERE address_id = ' . (int) $address_id;

The first PHP statement creates an SQL expression with the numeric strings '54321' and '67890', and the second statement an expression with the true integer values 54321 and 67890:

thumb_down Incorrect:
UPDATE sc_addresses
   SET customer_id = '54321'
 WHERE address_id  = '67890';
thumb_up Correct:
UPDATE sc_addresses
   SET customer_id = 54321
 WHERE address_id  = 67890;

Don’t close and immediately re-open PHP tags

A common mistake in PHP templates and MVC views is closing and immediately re-opening PHP-tags.

thumb_down Incorrect:
<?php echo $header; ?><?php echo $menu; ?>
thumb_down Incorrect:
<?php echo $header; ?>
<?php echo $menu; ?>
thumb_up Correct:
<?php
echo $header;
echo $menu;
?>
thumb_up Correct:
<?php
echo $header, $menu;
?>
thumb_up Correct:
<?php echo $header, $menu; ?>

Return results early

Once the result of a PHP method or function has been established, it SHOULD be returned. The examples below demonstrate this may save memory and computations.

thumb_down Incorrect:
public function hasDownload()
{
    $download = false;

    foreach ($this->getProducts() as $product) {
        if ($product['download']) {
            $download = true;
            break;
        }
    }

    return $download;
}
thumb_up Correct:
public function hasDownload()
{
    foreach ($this->getProducts() as $product) {
        if ($product['download']) {
            return true;
        }
    }
    return false;
}

Adding a temporary variable and two lines of code for a simple true or false does not really make sense. First breaking from an if nested in a foreach loop doesn’t make much sense either if you can just as well return the result immediately.

One of the reasons to return results at the end of a PHP method, is that no return can ever be overlooked. With a single return at the end, a method has a single point of exit, much like the method signature with the function parameters serves as a single point of entry.

However, if there is indeed a risk that a return might be overlooked, then the method may be too long. The function then probably can be split in multiple functions, with each function handling one of the return cases.

With a type declaration for the return value, introduced in PHP 7, there is no need to search for possible return values inside a method. The $download variable in the first, incorrect example illustrates that introducing an extra variable for the result is no guarantee for clean code: the return $download incorrectly suggests that a download will be returned.

thumb_up Recommended:
public function hasDownload(): bool
{
    foreach ($this->getProducts() as $product) {
        if ($product['download']) return true;
    }
    return false;
}