Performance guidelines
by Ward van der Put
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.
Incorrect:
setcookie('language', $lang, time() + 60 * 60 * 24 * 30, '/');
Correct:
setcookie('language', $lang, time() + 2592000, '/');
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.
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;
}
}
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
orprotected
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 inWHERE
conditions orORDER BY
statements - others columns used in
WHERE
conditions - others columns used in
ORDER BY
statements VARCHAR
columns with a variable length- large
TEXT
andBLOB
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.
Incorrect:
`date_created` DATETIME NOT NULL
Correct:
`date_created` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
Incorrect:
`date_modified` DATETIME NOT NULL
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_
for the initial INSERT
query and the modification date date_
to
ON UPDATE CURRENT_
for all subsequent
UPDATE
queries:
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_
in
DEFAULT
or ON UPDATE
clause.
The workaround currently implemented in StoreCore is to set the
DEFAULT
value for the initial INSERT
timestamp
to '0000-00-00 00:00:00'
and only use the
CURRENT_
for a subsequent ON UPDATE
:
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.
Incorrect:
$sql = "
UPDATE sc_addresses
SET customer_id = '" . (int) $customer_id . "'
WHERE address_id = '" . (int) $address_id . "'";
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
:
Incorrect:
UPDATE sc_addresses
SET customer_id = '54321'
WHERE address_id = '67890';
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.
Incorrect:
<?php echo $header; ?><?php echo $menu; ?>
Incorrect:
<?php echo $header; ?>
<?php echo $menu; ?>
Correct:
<?php
echo $header;
echo $menu;
?>
Correct:
<?php
echo $header, $menu;
?>
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.
Incorrect:
public function hasDownload()
{
$download = false;
foreach ($this->getProducts() as $product) {
if ($product['download']) {
$download = true;
break;
}
}
return $download;
}
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.
Recommended:
public function hasDownload(): bool
{
foreach ($this->getProducts() as $product) {
if ($product['download']) return true;
}
return false;
}