PHP: Code Quality with Custom Tooling Extensions

After many years of using PHPStan, PHP-CS-Fixer, PHP_CodeSniffer, … I will give you one advice: add your own custom code to extend your Code-Quality-Tooling.

Nearly every project has custom code that procures the real value for the product / project, but this custom code itself is often not really improved by PHP-CS-Fixer, PHPStan, Psalm, and other tools. The tools do not know how this custom code is working so that we need to write some extensions for ourselves.

Example: At work, we have some Html-Form-Element (HFE) classes that used some properties from our Active Record classes, and back in the time we used strings to connect both classes. :-/

Hint: Strings are very flexible, but also awful to use programmatically in the future. I would recommend avoiding plain strings as much as possible.

1. Custom PHP-CS-Fixer

So, I wrote a quick script that will replace the strings with some metadata. The big advantage is that this custom PHP-CS-Fixer will also automatically fix code that will be created in the future, and you can apply / check this in the CI-pipline or e.g. in a pre-commit hook or directly in PhpStorm.

<?php

declare(strict_types=1);

use PhpCsFixer\Tokenizer\Analyzer\ArgumentsAnalyzer;
use PhpCsFixer\Tokenizer\Analyzer\FunctionsAnalyzer;
use PhpCsFixer\Tokenizer\Token;
use PhpCsFixer\Tokenizer\Tokens;

final class MeerxUseMetaFromActiveRowForHFECallsFixer extends AbstractMeerxFixerHelper
{

/**
* {@inheritdoc}
*/
public function getDocumentation(): string
{
return 'Use ActiveRow->m() for "HFE_"-calls, if it is possible.';
}

/**
* {@inheritdoc}
*/
public function getSampleCode(): string
{
return <<<'PHP'
<?php

$element = UserFactory::singleton()->fetchEmpty();

$foo = HFE_Date::Gen($element, 'created_date');
PHP;
}

public function isRisky(): bool
{
return true;
}

/**
* {@inheritdoc}
*/
public function isCandidate(Tokens $tokens): bool
{
return $tokens->isTokenKindFound(\T_STRING);
}

public function getPriority(): int {
// must be run after NoAliasFunctionsFixer
// must be run before MethodArgumentSpaceFixer
return -1;
}

protected function applyFix(SplFileInfo $file, Tokens $tokens): void
{
if (v_str_contains($file->getFilename(), 'HFE_')) {
return;
}

$functionsAnalyzer = new FunctionsAnalyzer();

// fix for "HFE_*::Gen()"
foreach ($tokens as $index => $token) {
$index = (int)$index;

// only for "Gen()"-calls
if (!$token->equals([\T_STRING, 'Gen'], false)) {
continue;
}

// only for "HFE_*"-classes
$object = (string)$tokens[$index - 2]->getContent();
if (!v_str_starts_with($object, 'HFE_')) {
continue;
}

if ($functionsAnalyzer->isGlobalFunctionCall($tokens, $index)) {
continue;
}

$argumentsIndices = $this->getArgumentIndices($tokens, $index);

if (\count($argumentsIndices) >= 2) {
[
$firstArgumentIndex,
$secondArgumentIndex
] = array_keys($argumentsIndices);

// If the second argument is not a string, we cannot make a swap.
if (!$tokens[$secondArgumentIndex]->isGivenKind(\T_CONSTANT_ENCAPSED_STRING)) {
continue;
}

$content = trim($tokens[$secondArgumentIndex]->getContent(), '\'"');
if (!$content) {
continue;
}

$newContent = $tokens[$firstArgumentIndex]->getContent() . '->m()->' . $content;

$tokens[$secondArgumentIndex] = new Token([\T_CONSTANT_ENCAPSED_STRING, $newContent]);
}
}
}

/**
* @param Token[]|Tokens $tokens <phpdoctor-ignore-this-line/>
* @param int $functionNameIndex
*
* @return array<int, int> In the format: startIndex => endIndex
*/
private function getArgumentIndices(Tokens $tokens, $functionNameIndex): array
{
$argumentsAnalyzer = new ArgumentsAnalyzer();

$openParenthesis = $tokens->getNextTokenOfKind($functionNameIndex, ['(']);
$closeParenthesis = $tokens->findBlockEnd(Tokens::BLOCK_TYPE_PARENTHESIS_BRACE, $openParenthesis);

// init
$indices = [];

foreach ($argumentsAnalyzer->getArguments($tokens, $openParenthesis, $closeParenthesis) as $startIndexCandidate => $endIndex) {
$indices[$tokens->getNextMeaningfulToken($startIndexCandidate - 1)] = $tokens->getPrevMeaningfulToken($endIndex + 1);
}

return $indices;
}
}

To use your custom fixes, you can register and enable them: https://cs.symfony.com/doc/custom_rules.html  

Example-Result:


$fieldGroup->addElement(HFE_Customer::Gen($element, 'customer_id'));

// <- will be replaced with ->

$fieldGroup->addElement(HFE_Customer::Gen($element, $element->m()->customer_id));

Hint: There are many examples for PHP_CodeSniffer and Fixer Rules on GitHub, you can often pick something that fits 50-70% for your use-case and then modify it for your needs.

The “m()” method looks like this and will call the simple “ActiveRowMeta”-class. This class will return the property name itself instead of the real value.

/**
* (M)ETA
*
* @return ActiveRowMeta|mixed|static
* <p>
* We fake the return "static" here because we want auto-completion for the current properties in the IDE.
* <br><br>
* But here the properties contains only the name from the property itself.
* </p>
*
* @psalm-return object{string,string}
*/
final public function m()
{
return (new ActiveRowMeta())->create($this);
}
<?php

final class ActiveRowMeta
{
/**
* @return static
*/
public function create(ActiveRow $obj): self
{
/** @var static[] $STATIC_CACHE */
static $STATIC_CACHE = [];

// DEBUG
// var_dump($STATIC_CACHE);

$cacheKey = \get_class($obj);
if (!empty($STATIC_CACHE[$cacheKey])) {
return $STATIC_CACHE[$cacheKey];
}

foreach ($obj->getObjectVars() as $propertyName => $propertyValue) {
$this->{$propertyName} = $propertyName;
}

$STATIC_CACHE[$cacheKey] = $this;

return $this;
}

}

2. Custom PHPStan Extension

In the next step, I added a DynamicMethodReturnTypeExtension for PHPStan, so that the static code analyze knows the type of the metadata + I still have auto-completion in the IDE via phpdocs.

Note: Here I’ve also made the metadata read-only, so we can’t misuse the metadata.

<?php

declare(strict_types=1);

namespace meerx\App\scripts\githooks\StandardMeerx\PHPStanHelper;

use PhpParser\Node\Expr\MethodCall;
use PHPStan\Analyser\Scope;
use PHPStan\Reflection\MethodReflection;
use PHPStan\Type\Type;

final class MeerxMetaDynamicReturnTypeExtension implements \PHPStan\Type\DynamicMethodReturnTypeExtension
{

public function getClass(): string
{
return \ActiveRow::class;
}

public function isMethodSupported(MethodReflection $methodReflection): bool
{
return $methodReflection->getName() === 'm';
}

/**
* @var \PHPStan\Reflection\ReflectionProvider
*/
private $reflectionProvider;

public function __construct(\PHPStan\Reflection\ReflectionProvider $reflectionProvider)
{
$this->reflectionProvider = $reflectionProvider;
}

public function getTypeFromMethodCall(
MethodReflection $methodReflection,
MethodCall $methodCall,
Scope $scope
): Type
{
$exprType = $scope->getType($methodCall->var);

$staticClassName = $exprType->getReferencedClasses()[0];
$classReflection = $this->reflectionProvider->getClass($staticClassName);

return new MeerxMetaType($staticClassName, null, $classReflection);
}
}
<?php

declare(strict_types=1);

namespace meerx\App\scripts\githooks\StandardMeerx\PHPStanHelper;

use PHPStan\Reflection\ClassMemberAccessAnswerer;
use PHPStan\Type\ObjectType;

final class MeerxMetaType extends ObjectType
{

public function getProperty(string $propertyName, ClassMemberAccessAnswerer $scope): \PHPStan\Reflection\PropertyReflection
{
return new MeerxMetaProperty($this->getClassReflection());
}

}
<?php

declare(strict_types=1);

namespace meerx\App\scripts\githooks\StandardMeerx\PHPStanHelper;

use PHPStan\Reflection\ClassReflection;
use PHPStan\TrinaryLogic;
use PHPStan\Type\NeverType;
use PHPStan\Type\StringType;

final class MeerxMetaProperty implements \PHPStan\Reflection\PropertyReflection
{

private ClassReflection $classReflection;

public function __construct(ClassReflection $classReflection)
{
$this->classReflection = $classReflection;
}

public function getReadableType(): \PHPStan\Type\Type
{
return new StringType();
}

public function getWritableType(): \PHPStan\Type\Type
{
return new NeverType();
}

public function isWritable(): bool
{
return false;
}

public function getDeclaringClass(): \PHPStan\Reflection\ClassReflection
{
return $this->classReflection;
}

public function isStatic(): bool
{
return false;
}

public function isPrivate(): bool
{
return false;
}

public function isPublic(): bool
{
return true;
}

public function getDocComment(): ?string
{
return null;
}

public function canChangeTypeAfterAssignment(): bool
{
return false;
}

public function isReadable(): bool
{
return true;
}

public function isDeprecated(): \PHPStan\TrinaryLogic
{
return TrinaryLogic::createFromBoolean(false);
}

public function getDeprecatedDescription(): ?string
{
return null;
}

public function isInternal(): \PHPStan\TrinaryLogic
{
return TrinaryLogic::createFromBoolean(false);
}
}

Summary

Think about your custom code and how you can improve it, use your already used tools and extend it to understand your code. Sometimes it’s easy, and you can add some modern PHPDocs or you need to go down the rabbit hole and implement some custom stuff, but at last it will help your software, your team and your customers.

Timeout Problems: Web Server + PHP

What?

First there is an HTTP request and that will hit your Web server, then it will pass the request via TCP- or UNIT-Socket via FastCGI to your PHP-FPM Daemon, here we will start a new PHP process and in this process we will connect e.g. to the database and run some queries.

PHP-Request

The Problem!

There are different timeout problems here because we connect different pieces together and this parts need to communicate. But what if one of the pieces does not respond in a given time or, even more bad, if one process is running forever like a bad SQL-query.

Understand your Timeouts.

Timeouts are a way to limit the time that a request can run, and otherwise an attacker could simply run a denial-of-service with a simple request. But there are many configurations in several layers: Web server, PHP, application, database, curl, …

– Web server

Mostly you will use Apache or Nginx as Web server and in the end it makes not really a difference, there are different timeout settings, but the idea is almost the same: The Web server will stop the execution and kills the PHP process, now you got a 504 HTTP error (Gateway Timeout) and you will lose your stack trace and error-tracking because we killed our application in the middle of nothing. So, we should keep the Web server running as long as needed.

“`grep -Ri timeout /etc/apache2/“`

/etc/apache2/conf-enabled/timeout.conf:Timeout 60

/etc/apache2/mods-available/reqtimeout.conf:<IfModule reqtimeout_module>

/etc/apache2/mods-available/reqtimeout.conf: # mod_reqtimeout limits the time waiting on the client to prevent an

/etc/apache2/mods-available/reqtimeout.conf: # configuration, but it may be necessary to tune the timeout values to

/etc/apache2/mods-available/reqtimeout.conf: # mod_reqtimeout per virtual host.

/etc/apache2/mods-available/reqtimeout.conf: # Note: Lower timeouts may make sense on non-ssl virtual hosts but can

/etc/apache2/mods-available/reqtimeout.conf: # cause problem with ssl enabled virtual hosts: This timeout includes

/etc/apache2/mods-available/reqtimeout.conf: RequestReadTimeout header=20-40,minrate=500

/etc/apache2/mods-available/reqtimeout.conf: RequestReadTimeout body=10,minrate=500

/etc/apache2/mods-available/reqtimeout.load:LoadModule reqtimeout_module /usr/lib/apache2/modules/mod_reqtimeout.so

/etc/apache2/mods-available/ssl.conf: # to use and second the expiring timeout (in seconds).

/etc/apache2/mods-available/ssl.conf: SSLSessionCacheTimeout 300

/etc/apache2/conf-available/timeout.conf:Timeout 60

/etc/apache2/apache2.conf:# Timeout: The number of seconds before receives and sends time out.

/etc/apache2/apache2.conf:Timeout 60

/etc/apache2/apache2.conf:# KeepAliveTimeout: Number of seconds to wait for the next request from the

/etc/apache2/apache2.conf:KeepAliveTimeout 5

/etc/apache2/mods-enabled/reqtimeout.conf:<IfModule reqtimeout_module>

/etc/apache2/mods-enabled/reqtimeout.conf: # mod_reqtimeout limits the time waiting on the client to prevent an

/etc/apache2/mods-enabled/reqtimeout.conf: # configuration, but it may be necessary to tune the timeout values to

/etc/apache2/mods-enabled/reqtimeout.conf: # mod_reqtimeout per virtual host.

/etc/apache2/mods-enabled/reqtimeout.conf: # Note: Lower timeouts may make sense on non-ssl virtual hosts but can

/etc/apache2/mods-enabled/reqtimeout.conf: # cause problem with ssl enabled virtual hosts: This timeout includes

/etc/apache2/mods-enabled/reqtimeout.conf: RequestReadTimeout header=20-40,minrate=500

/etc/apache2/mods-enabled/reqtimeout.conf: RequestReadTimeout body=10,minrate=500

/etc/apache2/mods-enabled/reqtimeout.load:LoadModule reqtimeout_module /usr/lib/apache2/modules/mod_reqtimeout.so

/etc/apache2/mods-enabled/ssl.conf: # to use and second the expiring timeout (in seconds).

/etc/apache2/mods-enabled/ssl.conf: SSLSessionCacheTimeout 300

Here you can see all configurations for Apache2 timeouts, but we only need to change etc/apache2/conf-enabled/timeout.conf`` because it will overwrite `/etc/apache2/apache2.conf` anyway.

PS: Remember to reload / restart your Web server after you change the configurations.

If we want to show the user at least a custom error page, we could add something like:

ErrorDocument503 /error.php?errorcode=503
ErrorDocument 504 /error.php?errorcode=504

… into our Apache configuration or in a .htaccess file, so that we can still use PHP to show an error page, also if the requested PHP call was killed. The problem here is that we will lose the error message / stack trace / request etc. from the error, and we can’t send e.g. an error into our error logging system. (take a look at sentry, it’s really helpful)

– PHP-FPM

Our PHP-FPM (FastCGI Process Manager) pool can be configured with a timeout (request-terminate-timeout), but just like the Web server setting, this will kill the PHP worker in the middle of the process, and we can’t handle the error in PHP itself. There is also a setting (process_control_timeout) that tells the child processes to wait for this much time before executing the signal received from the parent process, but I am uncertain if this is somehow helpfully here? So, our error handling in PHP can’t catch / log / show the error, and we will get a 503 HTTP error (Service Unavailable) in case of a timeout.

Shutdown functions will not be executed if the process is killed with a SIGTERM or SIGKILL signal. :-/

Source: register_shutdown_function

PS: Remember to reload / restart your PHP-FPM Daemon after you change the configurations.

– PHP

The first idea from most of us would be maybe to limit the PHP execution time itself, and we are done, but that sounds easier than it is because `max_execution_time` ignores time spent on I/O (system commands e.g. `sleep()`, database queries (SELECT SLEEP(100)). But these are the bottlenecks of nearly all PHP applications, PHP itself is fast but the external called stuff isn’t.

Theset_time_limit()function and the configuration directive max_execution_time only affect the execution time of the script itself. Any time spent on activity that happens outside the execution of the script such as system calls using system(), stream operations, database queries, etc. is not included when determining the maximum time that the script has been running. This is not true on Windows where the measured time is real.

Source: set_time_limit

– Database (MySQLi)

Many PHP applications spend most of their time waiting for some bad SQL queries, where the developer missed adding the correct indexes and because we learned that the PHP max execution time did not work for database queries, we need one more timeout setting here.

There is the MYSQLI_OPT_CONNECT_TIMEOUT and MYSQLI_OPT_READ_TIMEOUT (Command execution result timeout in seconds. Available as of PHP 7.2.0. – mysqli.options) setting, and we can use that to limit the time for our queries.

In the end you will see a “Errno: 2006 | Error: MySQL server has gone away” error in your PHP application, but this error can be caught / reported, and the SQL query can be fixed, otherwise the Apache or PHP-FPM would kill the process, and we do not see the error because our error handler can’t handle it anyway.

Summary:

It’s complicated. PHP is not designed for long execution and that is good as it is, but if you need to increase the timeout it will be more complicated than I first thought. You need for example different “timeout”-code for testing different settings:

// DEBUG: long-running sql-call
// Query(‘SELECT SLEEP(600);’);

// DEBUG: long-running system-call
// sleep(600);

// DEBUG: long-running php-call
// while (1) { } // infinite loop

Solution:

We can combine different timeout, but the timeout from the called commands e.g. database, curl, etc. will be combined with the timeout from PHP (max_execution_time) itself. The timeout from the Web server (e.g. Apache2: Timeout) and from PHP-FPM (request_terminate_timeout) need to be longer than the combined timeout from the application so that we still can use our PHP error handler.

e.g.: ~ 5 min. timeout

  1. MySQL read timeout: 240s ⇾ 4 min.
    link->options(MYSQLI_OPT_READ_TIMEOUT, 240);
  2. PHP timeout: 300s ⇾ 5 min.
    max_execution_time = 300
  3. Apache timeout: 360s ⇾ 6 min.
    Timeout 360
  4. PHP-FPM: 420s ⇾ 7 min.
    request_terminate_timeout = 420

 

Links:

Prepare your PHP Code for Static Analysis

Three years ago I got a new job as PHP developer, before that I called myself web developer because I build ReactJS, jQuery, CSS, HTML, … and PHP  stuff for a web agency. So now I am a full-time PHP developer and I converted a non typed  (no PHPDoc + no native types) project with ~ 10.000 classes into a project with ~ 90% type coverage. Here is what I have learned.

1. Write code with IDE autocompletion support.

If you have autocompletion in the IDE most likely the Static Analysis can understand the code as well. 

Example:

bad:

->get('DB_Connection', true, false);

still bad:

->get(DB_Connection::class);

good:

getDbConnection(): DB_Connection

2. Magic in Code is bad for the long run!

Magic methods (__get, __set, …) for example can help to implement new stuff very fast, but the problem is nobody will understand it, you will have no autocompletion, no refactoring options, other developers will need more time to read and navigate in the code and in the end it will cost you much more time than you can save with it.

3. Break the spell on Magic Code …

… by explaining to everyone (Devs > IDE > Tools) what it does.

Example 1:

We use a simple Active Record Pattern, but we put all SQL stuff into the Factory classes, so that the Active Record class can be simple. (Example) But because of missing support for Generics we had no autocompletion without adding many dummy methods into the classes. So one of my first steps was to introduce a “PhpCsFixer” that automatically adds the missing methods of the parent class with the correct types via “@method”-comments into these classes. 

Example 2:

Sometimes you can use more modern PHPDocs to explain the function. Take a look at the “array_first” function in the linked Post.

Example 3:

/**
* Return an array which has the Property-Values of the given Objects as Values.
*
* @param object[] $ObjArray
* @param string $PropertyName
* @param null|string $KeyPropertyName if given uses this Property as key for the returned Array otherwise the keys from the
* given array are used
*
* @throws Exception if no property with the given name was found
*
* @return array
*/
function propertyArray($ObjArray, $PropertyName, $KeyPropertyName = null): array {
// init
$PropertyArray = [];

foreach ($ObjArray as $key => $Obj) {
if (!\property_exists($Obj, $PropertyName)) {
throw new Exception('No Property with Name ' . $PropertyName . ' in Object Found - Value');
}

$usedKey = $key;
if ($KeyPropertyName) {
if (!\property_exists($Obj, $KeyPropertyName)) {
throw new Exception('No Property with Name ' . $PropertyName . ' in Object Found - Key');
}
$usedKey = $Obj->{$KeyPropertyName};
}

$PropertyArray[$usedKey] = $Obj->{$PropertyName};
}

return $PropertyArray;
}

Sometimes it’s hard to describe the specific output types, so here you need to extend the  functions of your Static Code Analyze Tool, so that it knows what you are doing. ⇾ for example here you can find a solution for PHPStan ⇽  but there is still no support for the IDE and so maybe it’s not the best idea to use magic like that at all. And I am sure it’s more simple to use specific and simple methods instead:  e.g. BillCollection->getBillDates()

4. Try to not use strings for the code.

Strings are simple and flexible, but they are also bad for the long run. Mostly strings are used because it looks like a simple solution, but often they are redundant, you will have typos everywhere and the IDE and/or Static Analysis can’t analyze them because it’s just text.

Example:

bad: 

AjaxEditDate::generator($bill->bill_id, $bill->date, 'Bill', 'date');
  • “Bill” ⇾ is not needed here, we can call e.g. get_class($bill) in the “generator” method
  • “date” ⇾ is not needed here, we can fetch the property name from the class
  • “$bill->bill_id” ⇾ is not needed here, we can get the primary id value from the class

good:

AjaxEditDate::generator($bill, $bill->m()->date);

5. Automate stuff via git hook and check it via CI server.

Fixing bad code is only done if you disallow the same bad code for the future. With a pre-commit hook for git it’s simple to run these checks, but you need to check it again in the CI server because the developers can simply skip these checks.

Example:

I introduced a check for disallowing global variables (global $foo && $GLOBALS[‘foo’]) via “PHP_CodeSniffer”. 

Links:

6. Use array shapes (or collections) if you need to return an array, please.

Array shapes are like arrays but with fixed keys, so that you can define the types of each key in the PHPDocs.

You will have autocompletion for the IDE + all other devs can see what the method will return + you will notice if you’re better retuning an object because it will be very hard to describe the output for complex data structures and Static Analysis can use and check the types. 

Example:

/**
* @return array{
* missing: array<ShoppingCartLib::TAB_*,string>,
* disabled: array<ShoppingCartLib::TAB_*,string>
* }
*/
private static function getShoppingCartTabStatus(): array {
...
}

Generics in PHP via PHPDocs

If you did not know that you can use Generics in PHP or you do not exactly know how to use it or why you should use it, then the next examples are for you.

Type variables via @template

The @template tag allows classes and functions to declare a generic type parameter. The next examples starts with simple functions, so that we understand how it works, and then we will see the power of this in classes.


A dummy function that will return the input.

https://phpstan.org/r/1922279b-9786-4523-939d-dddcfd4ebb86

    <?php    

    /**
     * @param \Exception $param
     * @return \Exception
     *
     * @template T of \Exception
     * @psalm-param T $param
     * @psalm-return T
     */
    function foo($param) { ... }

    foo(new \InvalidArgumentException()); // The static-analysis-tool knows that 
                                          // the type is still "\InvalidArgumentException" 
                                          // because of the type variable.

@template T of \Exception // here we create a new type variable, and we force that it must be an instance of \Exception

@phpstan-param T $param // here we say that the static-analysis-tool need to remember the type that this variable had before (you can use @psalm-* or @phpstan-* both works with both tools)

@phpstan-return T // and that the return type is the same as the input type 


A simple function that gets the first element of an array or a fallback. 

In the @param PHPDocs we write “mixed” because this function can handle different types. But this information is not very helpful if you want to understand programmatically what the code does, so we need to give the static-analysis-tools some more information. 

https://phpstan.org/r/1900a2af-f5c1-4942-939c-409928a5ac4a

    <?php
     
    /**
     * @param array<mixed> $array
     * @param mixed        $fallback <p>This fallback will be used, if the array is empty.</p>
     *
     * @return mixed|null
     *
     * @template TFirst
     * @template TFirstFallback
     * @psalm-param TFirst[] $array
     * @psalm-param TFirstFallback $fallback
     * @psalm-return TFirst|TFirstFallback
     */
    function array_first(array $array, $fallback)
    {
        $key_first = array_key_first($array);
        if ($key_first === null) {
            return $fallback;
        }

        return $array[$key_first];
    }

    array_first([1, 2, 3], null); 

    if ($a === 'foo') { // The static-analysis-tool knows that 
                        // === between int|null and 'foo' will always evaluate to false.
	    // ...
    }

@template TFirst // we again define your typed variables

@template TFirstFallback // and one more because we have two inputs where we want to keep track of the types

@psalm-param TFirst[] $array // here we define that $array is an array of TFirst types

@psalm-param TFirstFallback $fallback // and that $fallback is some other type that comes into this function

@psalm-return TFirst|TFirstFallback // now we define the return type as an element of  the $array or the $fallback type 


 Very basic Active Record + Generics

The IDE support for generics is currently not there, :-/ so that we still need some hacks (see @method) for e.g. PhpStorm to have autocompletion.

https://phpstan.org/r/f88f5cd4-1bb9-4a09-baae-069fddb10b12

https://github.com/voku/phpstorm_issue_53352/tree/master/src/Framework/ActiveRecord

<?php

class ActiveRow
{
    /**
     * @var ManagedFactory<static>
     */
    public $factory;

    /**
     * @param Factory<ActiveRow>|ManagedFactory<static> $factory
     * @param null|array                                $row
     */
    public function __construct(Factory $factory, array $row = null) {
        $this->factory = &$factory;
    }
}

/**
 * @template T
 */
abstract class Factory
{
    /**
     * @var string
     *
     * @internal
     *
     * @psalm-var class-string<T>
     */
    protected $classname;

    /**
     * @return static
     */
    public static function create() {
        return new static();
    }
}

/**
 * @template  T
 * @extends   Factory<T>
 */
class ManagedFactory extends Factory
{
    /**
     * @param string $classname
     *
     * @return void
     *
     * @psalm-param class-string<T> $classname
     */
    protected function setClass(string $classname): void
    {
        if (\class_exists($classname) === false) {
            /** @noinspection ThrowRawExceptionInspection */
            throw new Exception('TODO');
        }

        if (\is_subclass_of($classname, ActiveRow::class) === false) {
            /** @noinspection ThrowRawExceptionInspection */
            throw new Exception('TODO');
        }

        $this->classname = $classname;
    }

    // ...
}

final class Foo extends ActiveRow {

    public int $foo_id;

    public int $user_id;

    // --------------------------------------
    // add more logic here ...
    // --------------------------------------
}

/**
 * @method Foo[] fetchAll(...)
 *
 * @see Foo
 *
 * // warning -> do not edit this comment by hand, it's auto-generated and the @method phpdocs are for IDE support       
 * //         -> https://gist.github.com/voku/3aba12eb898dfa209a787c398a331f9c
 *
 * @extends ManagedFactory<Foo>
 */
final class FooFactory extends ManagedFactory
{
    // -----------------------------------------------
    // add sql stuff here ...
    // -----------------------------------------------
}

A more complex collection example.

In the end we can extend the “AbstractCollection” and the static-analysis-tools knows the types of all the methods. 

https://github.com/voku/Arrayy/tree/master/src/Type

/**
 * @template TKey of array-key
 * @template T
 * @template-extends \ArrayObject<TKey,T>
 * @template-implements \IteratorAggregate<TKey,T>
 * @template-implements \ArrayAccess<TKey|null,T>
 */
class Arrayy extends \ArrayObject implements \IteratorAggregate, \ArrayAccess, \Serializable, \JsonSerializable, \Countable
{ ... }

/**
 * @template TKey of array-key
 * @template T
 * @template-extends \IteratorAggregate<TKey,T>
 * @template-extends \ArrayAccess<TKey|null,T>
 */
interface CollectionInterface extends \IteratorAggregate, \ArrayAccess, \Serializable, \JsonSerializable, \Countable
{ ... }

/**
 * @template   TKey of array-key
 * @template   T
 * @extends    Arrayy<TKey,T>
 * @implements CollectionInterface<TKey,T>
 */
abstract class AbstractCollection extends Arrayy implements CollectionInterface
{ ... }

/**
 * @template TKey of array-key
 * @template T
 * @extends  AbstractCollection<TKey,T>
 */
class Collection extends AbstractCollection
{ ... }

Links:

https://phpstan.org/blog/generics-in-php-using-phpdocs

https://psalm.dev/docs/annotating_code/templated_annotations/

https://stitcher.io/blog/php-generics-and-why-we-need-them

❤️ Simple PHP Code Parser

It based on code from “JetBrains/phpstorm-stubs” but instead of Php-Reflection we now use nikic/PHP-Parser, BetterReflection, phpDocumentor and PHPStan/phpdoc-parser internally. So, you can get even more information about the code. For example, psalm- / phpstan-phpdoc annotations or inheritdoc from methods.

Install:

composer require voku/simple-php-code-parser

Link:

voku/Simple-PHP-Code-Parser

More:


Example: get value from define in “\foo\bar” namespace

$code = '
  <?php
  namespace foo\bar;
  define("FOO_BAR", "Lall");
';

$phpCode = PhpCodeParser::getFromString($code);
$phpConstants = $phpCode->getConstants();

$phpConstants['\foo\bar\FOO_BAR']->value; // 'Lall'

Example: get information about @property phpdoc from a class

$code = '
  <?php
  /** 
   * @property int[] $foo 
   */
  abstract class Foo { }
';

$phpCode = PhpCodeParser::getFromString($code);
$phpClass = $phpCode->getClass('Foo');

$phpClass->properties['foo']->typeFromPhpDoc); // int

Example: get classes from a string (or from a class-name or from a file or from a directory)

$code = '
<?php
namespace voku\tests;
class SimpleClass {}
$obja = new class() {};
$objb = new class {};
class AnotherClass {}
';

$phpCode = \voku\SimplePhpParser\Parsers\PhpCodeParser::getFromString($code);
$phpClasses = $phpCode->getClasses();

var_dump($phpClasses['voku\tests\SimpleClass']); // "PHPClass"-object

Arrayy: A Quick Overview of map(), filter(), and reduce()

Arrayy: A PHP array manipulation library. Compatible with PHP 7+

The next examples are using the php array manipulation library “Arrayy” which is using generators internally for many operations.

https://github.com/voku/Arrayy

StringCollection::create(['Array', 'Array'])->unique()->append('y')->implode(); // Arrayy


map: transform all values in the collection

StringCollection::create(['foo', 'Foo'])->map('mb_strtoupper'); 

// StringCollection['FOO', 'FOO']

filter: pass all values to the truth test

$closure = function ($value) {
    return $value % 2 !== 0;
}
IntCollection::create([1, 2, 3, 4])->filter($closure); 

// IntCollection[0 => 1, 2 => 3]

reduce: transform all values into a new result

IntCollection::create([1, 2, 3, 4])->reduce(
    function ($carry, $item) {
        return $carry * $item;
    },
    1
); 

// IntCollection[24]

Modern PHPDoc Annotations

We will start very simple with PhpStorm and default PHPDoc, then we will increase the complexity step by step until we have auto-completion for array keys directly from the database with generics, immutable and type safety support.

1.0 PhpStorm & auto-generate PHPDoc blocks

„For documentation comments, PhpStorm provides completion that is enabled by default. PhpStorm creates stubs of „PHPDoc blocks“ when you type the /** opening tag and press Enter, or press Alt+Insert and appoint the code construct (a class, a method, a function, and so on) to document. Depending on your choice, PhpStorm will create the required tags or add an empty documentation stub.“

https://www.jetbrains.com/help/phpstorm/phpdoc-comments.html

Code:

/**
 * @param array $row
 *
 * @return array
 */
abstract function formatRow(array $row): array;

1.1 Return $this|static|self

It‘s quite annoying that php itself currently only have „self“ as return type (https://wiki.php.net/rfc/static_return_type) for the current class. Because of „late static binding“ you can use „static“ in your code to refer to the class a method was actually called on, even if the method is inherited. But in PHPDoc you can already use:

  • @return $this: if you really return $this (e.g. for fluent interface)
  • @return static: refer to the class a method was actually called on
  • @return self: refer to the class a method was written in

Code:

/**
 * @return static
 */
abstract function getFoo(): self;

1.2 New (and not that new) Array Syntax

PhpStorm and (PHPStan & Psalm) are supporting some new (and some not that new) array syntax for PHPDoc types, but for now PhpStorm will not auto-generate this types. 

Examples:

  • int[]: an array with only INT values – [1, 4, 6, 8, 9, …]
  • array<int, int>: an array with only INT values – [4 => 1, 8 => 4, 12 => 6, …]
  • string[]: an array with only STRING values – [„foo“, „bar“, …]
  • array<int, string>: an array with only STRING values – [4 => „foo“, 8 => „bar“, …]
  • Order[]: an array with only „Order“-Object values – [Order, Order, …]
  • array<int|string, Order>: an array with INT or STRING as key and „Order“-Object values – [4 => Order, ‘foo‘ => Order, …]
  • array<int|string, mixed>: an array with INT or STRING as key and mixed as values – [1 => 1, 4 => „foo“, 6 => \stdClass, …]
  • array<int, array<int, string>>: an array with INT as key and and an array (with INT as key and string as value) as values – [1 => [1 => „foo“], 4 => [1 => 4], …]
  • array<int, string[]>: an array with INT as key and and an array (with INT as key and string as value) as values – [1 => [„foo“, „lall“], 4 => [„öäü“, „bar“], …]
  • array{output: string, debug: string}: an array with the key “output” and “debug”, the values are STRING values – [‘output’ => ‘foo’, ‘debug’ => ‘bar’]
  • array<int, array{output: string, debug: string}>: an array with the key “output” and “debug”, the values are STRING values – [1 => [‘output’ => ‘foo’, ‘debug’ => ‘bar’], 3 => [‘output’ => ‘foo’, ‘debug’ => ‘bar’], …]

Examples (@psalm-* || @phpstan-*): PHPStan can also use “psalm-*” prefixed annotations and Psalm understands “phpstan-*” annotations.

  • list<array{output: string, debug: string}>: an array with the key “output” and “debug”, the values are STRING values – [0 => [‘output’ => ‘foo’, ‘debug’ => ‘bar’], 1 => [‘output’ => ‘foo’, ‘debug’ => ‘bar’], …]

list: represents continuous, integer-indexed arrays (always start from index zero) like: [“red”, “yellow”, “blue”] 

Live-Examples:

Psalm: https://psalm.dev/r/922d4ba5b1

PHPStan: https://phpstan.org/r/ce657ef4-9f18-46a1-b21a-e51e3a0e6d2d

Code:

/**
 * @param array<int|string, mixed> $row
 *
 * @return array<int|string, mixed>
 */
abstract function formatRow(array $row): array;

PhpStorm support?: Sadly PhpStorm did not have good support for these types, so that you often have to add „@psalm-*“ PHPDoc comments. For example PhpStorm will accept “array<int, Order>” but PhpStorm will not understand the PHPDoc, so that you need to add e.g. “@param Order[] $order” and “@psalm-param array<int, Order> $order”. 

Examples for PhoStorm + PHPStan || Psalm: 

/**
* @param Order[] $order
* @psalm-param array<int, Order> $order
*
* @return void
*/
public function fooOrder($order): void { ... }

// you could also use "..." here

/**
* @param Order ...$order
*
* @return void
*/
public function fooOrder(Order ...$order): void { ... }
/**
* @param int $foo_id
*
* @return Foo[]|Generator
* @psalm-return Generator&iterable<Foo>
*/
abstract function fetchYieldByFoo($foo_id): Generator;

1.3 Dynamic Autocompletion (+ data from your database) via deep-assoc-completion

If you have a method e.g. “formatRow($row)” you can use “getFieldArray()[0]” (data from the database – you have to connection the IDE with your database and your queries need to be analyzable by PhpStorm (take a look at the next screenshot) and combine static data from “getHeaderFieldArray()”, so that you have auto-completion from different sources.

Code:

/**
 * @param array<int|string, mixed> $row = $this->getFieldArray()[0] + $this->getHeaderFieldArray()
 *
 * @return array<int|string, mixed>
 */
abstract function formatRow(array $row): array;

more information + examples: https://github.com/klesun/deep-assoc-completion

1.4 Immutability Check via Static Code Analyses (via psalm)

And there is even more. :) You can add PHPDoc annotation that will check if you really use immutable classes or at least methods. Please read more here: https://psalm.dev/articles/immutability-and-beyond

Code:

/**
 * @param array<int|string, mixed> $row = $this->getFieldArray()[0] + $this->getHeaderFieldArray()
 *
 * @return array<int|string, mixed>
 *
 * @psalm-mutation-free
 */
abstract function formatRow(array $row): array;

Live-Example:

– Psalm: https://psalm.dev/r/5bac0a9a07

1.5 Generics in PHP via Static Code Analyses

We can also use Generics via code annotations. PHPStan & Psalm both support it, but Psalm’s support is more feature complete and both tools can use the „@psalm-“-syntax. Here comes some simple examples.

array_last: Will return the last array element from the $array (type: TLast) or the $fallback (type: TLastFallback). We tell the function that the types comes from the input parameters and that the input is an array of TLast or TLastFallback from the fallback.

/**
 * @param array<mixed> $array
 * @param mixed             $fallback <p>This fallback will be used, if the array is empty.</p>
 *
 * @return mixed|null
 *
 * @template TLast
 * @template TLastFallback
 * @psalm-param TLast[] $array
 * @psalm-param TLastFallback $fallback
 * @psalm-return TLast|TLastFallback
 */
function array_last(array $array, $fallback = null)
{
    $key_last = \array_key_last($array);
    if ($key_last === null) {
        return $fallback;
    }
return $array[$key_last]; }

array_first: Will return the first array element from the $array (type: TFirst) or the $fallback (type: TFirstFallback). We tell the function that the types comes from the input params and that the input is an array of TFirst or TFirstFallback from the fallback.

/**
 * @param array<mixed> $array
 * @param mixed             $fallback <p>This fallback will be used, if the array is empty.</p>
 *
 * @return mixed|null
 *
 * @template TFirst
 * @template TFirstFallback
 * @psalm-param TFirst[] $array
 * @psalm-param TFirstFallback $fallback
 * @psalm-return TFirst|TFirstFallback
 */
function array_first(array $array, $fallback = null)
{
    $key_first = array_key_first($array);
    if ($key_first === null) {
        return $fallback;
    }
return $array[$key_first]; }

So we can define „Templates“ and map input arguments on that types, this can be even more complex if you use it in a class context and you map the „Templates“ on class properties. But the logic will be the same.

Here is a more complex example: https://github.com/voku/Arrayy/blob/master/src/Collection/CollectionInterface.php

PhpStorm support?: Noop, sadly we need to hack this via „PHPSTORM_META“, so here is an example:

  • override(\array_filter(0), type(0)); // suppose first parameter type is MyClass[] then return type of array_filter will be MyClass[]
  • override(\array_reduce(0), elementType(0)); // suppose first parameter type is MyClass[] then return type of array_reduce will be MyClass

Read more here:

2.0 Resume

It‘s not perfect, and type check and auto-completion only with PHPDoc is not really what I expected for the year 2020. But it‘s working and I hope PhpStorm will bring more support for the new types annotations in the future.

More Links:

 

PHP: Arrays zusammenzuführen

In den folgenden Beispielen werden Standard-Funktionen von PHP gezeigt, um .

Ausgangswerte:

$array1 = [1 => 'one', 2 => 'two', 'foo' => 'bar1'];

$array2 = [3 => 'three', 4 => 'four', 6 => 'six', 'foo' => 'bar2'];

Wenn man Arrays mit “+” verbindet, werden die Werte in der Reihenfolge in welcher man diese Verbindet in das neue Array übernommen. Dabei sollte man jedoch beachten, dass gleiche Schlüssel (keys) nicht überschrieben werden.

$array2 + $array1;

—>

array
(
    [3] => three    // array2
    [4] => four     // array2
    [6] => six      // array2
    [foo] => bar2   // array2
    [1] => one      // array1
    [2] => two      // array1
)

Hier ein zweites Beispiel mit ‘+’, nun haben wir die ensprechenden Arrays jedoch umgekehrt, so dass der ‘foo’-Wert vom ersten Array behalten wird.

$array1 + $array2;

—>

array
(
    [1] => one      // array1
    [2] => two      // array1
    [foo] => bar1   // array1
    [3] => three    // array2
    [4] => four     // array2
    [6] => six      // array2
)

Diese kleine “foreach”-Schleife ist ein nachbau der ‘+’-Methode. Auch hier werden neue Werte angefügt, alte Werte jedoch behalten.

$union = $array1;
foreach ($array2 as $key => $value) {
  if (array_key_exists($key, $union) === false) {
    $union[$key] = $value;
  }
}

—>

array
(
    [1] => one      // array1
    [2] => two      // array1
    [foo] => bar1   // array1
    [3] => three    // array2
    [4] => four     // array2
    [6] => six      // array2
)


Und auch diese “foreach”-Schleife werden die Arrays vertauscht, so dass die Werte von ‘array2’ bestehen bleiben.

$union = $array2;
foreach ($array1 as $key => $value) {
  if (array_key_exists($key, $union) === false) {
    $union[$key] = $value;
  }
}

—>

array
(
    [3] => three    // array2
    [4] => four     // array2
    [6] => six      // array2
    [foo] => bar2   // array2
    [1] => one      // array1
    [2] => two      // array1
)

Die folgende PHP-Funktion (array_replace()) behält die Reihenfolge von ‘array1’, ersetzt jedoch Schlüssel (keys) welche ebenfalls in ‘array2’ vorkommen mit dessen Werten.

array_replace($array1, $array2);

—>

array
(
    [1] => one      // array1
    [2] => two      // array1
    [foo] => bar2   // array2 (position from array1)
    [3] => three    // array2
    [4] => four     // array2
    [6] => six      // array2
)

Hier wird ebenfalls ‘array_replace()’ verwendent, jedoch sind ‘array1’ und ‘array2’ wieder vertauscht und auch hier werden die Schlüssel (keys) beibehalten und die Position von ‘foo’ wird beibehalten.

array_replace($array2, $array1);

—>

array
(
    [3] => three    // array2
    [4] => four     // array2
    [6] => six      // array2
    [foo] => bar1   // array1 (position from array2)
    [1] => one      // array1
    [2] => two      // array1
)

Die Funktion ‘array_merge()’ vergibt neue Schlüssel (keys) für indizierte Arrays (0,1,2,3…). Somit fängt das neue Array nun mit dem Schlüssel “0” an.

array_merge($array1, $array2);

—>

array
(
    [0] => one      // array1 (with a new key)
    [1] => two      // array1 (with a new key)
    [foo] => bar2   // array2 (position from array1)
    [2] => three    // array2 (with a new key)
    [3] => four     // array2 (with a new key)
    [4] => six      // array2 (with a new key)
)

Auch hier wurden die Arrays wieder vertauscht und auch hier wurden die numerischen Schlüssel durch neue ersetzt.

array_merge($array2, $array1);

—>

array
(
    [0] => three    // array2 (with a new key)
    [1] => four     // array2 (with a new key)
    [2] => six      // array2 (with a new key)
    [foo] => bar1   // array1 (position from array2)
    [3] => one      // array1 (with a new key)
    [4] => two      // array1 (with a new key)
)

Die PHP-Library “Arrayy” (https://github.com/voku/Arrayy) …

Arrayy::create($array1)->mergePrependKeepIndex($array2);

—>

Arrayy {
  ["array":protected]=>
  array(6) {
    [1]=> one
    [2]=> two
    [foo]=> bar2
    [3]=> three
    [4]=> four
    [6]=> six
  }
}

Arrayy::create($array1)->mergePrependNewIndex($array2);

—>

Arrayy {
  ["array":protected]=>
  array(6) {
    [0]=> three
    [1]=> four
    [2]=> six
    [foo]=> bar1
    [3]=> one
    [4]=> two
  }
}

Links:

Blackfire PHP-Profiler: Fire up the Performance

Am Wochenende habe ich mal wieder ein neues Tool ausprobiert: “Blackfire” ist ebenso wie “Insight” von SensioLabs entwickelt und kann die Performance einer PHP-Anwendung “on the fly” analysieren und visualisieren.

Die Installation besteht aus folgenden Komponenten:

  • Probe: dies ist die PHP-Extension welche wir auf dem Server installieren
  • Agent: Dies ist ein Programm, welches ebenfalls auf dem Server installiert wird und die Performance-Daten zu blackfire.io weiterleitet, wo diese dann visualisiert werden
  • Client (optional): ein CLI-Tool um PHP-Profiling auf der Kommandozeile auszuführen
  • Companion: dies ist eine Chrome-Extension, welche auf Knopfdruck das PHP-Profiling ausführt
  • Website: die blackfire.io Webseite zur Darstellung der Ergebnisse

Die Installationsanleitung ist innerhalb von zirka 5 Minuten abgearbeitet und schon kann man schnell und bequem die Leistung / Performance von Änderungen testen und die entsprechenden Ergebnisse miteinander vergleichen.

blackfire.io
blackfire.io: Beispiel-Profile von “http://moelleken.org

 

PS: wer noch mehr Information benötigt sollte sich Xdebug lokal installieren bzw. aktivieren und die Profile-Daten anschließend via WinCacheGrind (Windows) oder direkt via KCachegrind (Linux) auswerten und visualisieren.

KCachegrind
KCachegrind

 

DO EPIC SHIT!

Slides | Demo (source)

Auf der OpenRheinRuhr (Messe mit Kongress rund um das Thema “Freie Software”) habe ich meinen ersten öffentlichen Vortrag gehalten. Dazu habe ich das Thema “Open Source Workflow für Webentwickler” gewählt und mit dem doch sehr einprägsamen Titel “DO EPIC SHIT!” angekündigt.

B16GnDZIcAA3aRb

Gegen alle Erwartungen bin ich pünklich angekommen, auch wenn die Bahn (GDL) gestreikt hat. Also hatte ich noch etwas Zeit, um noch einen `git commit` vorzubereiten, welcher dann live in dem Vortrag automatisch via Jenkins getestet und deployed werden konnte.

Und auch wenn ich den Vortrag nicht so beenden konnte wie ursprünglich geplant, da ich die Zeit irgendwie falsch eingeteilt hatte, hoffe ich trotzdem einiges nützliches gezeigt zu haben. Beim nächsten mal sollte ich dies wohl vor “echtem” Publikum testen, da die Zeit dann anscheinend anders verläuft. ;) Einige Erklärungen und Beispiele von der Demo-Webseite musste ich daher unterschlagen und reiche diese Infos im folgenden nach:

Backend:

Frontend:

Extras:

PS: Ich werde heute wieder auf der OpenRheinRuhr sein, da auch das heutige Programm sehr interessante Vorträge und Workshops biete. z.B.: DockerSaltStackImageMagick. Gegebenenfalls sieht man sich dort :) wünsche allen Beteiligten viel Spaß!