Static Code Analysis in PHP - Benjamin Cremer
Benjamin Cremer
@benjamincremer
Organizer of the PHP Usergroup Münster
Developer at CHECK24 Hotel
Not doing Symfony since 2017
Does not matter for todays topic
public function addNumbers(float $a, float $b): float
{
return $a - $b;
}
function foo() {
bar()
$baz
}
# Run PHP Syntax check (lint)
$ php -l example.php
PHP Parse error: syntax error, unexpected '$baz' (T_VARIABLE)
Errors parsing example.php
# "compile" code
# Syntax errors are also compile-time errors
$ php example.php
PHP Parse error: syntax error, unexpected '$baz' (T_VARIABLE)
Syntax Check with PHP Parallel Lint
Wrapper around php -l with several options for your convenience.
composer require --dev jakub-onderka/php-parallel-lint
./vendor/bin/parallel-lint -j 24 src/
interface Car
{
public function drive(): int;
}
class Tesla implements Car
{
public function drive(): string
{
return 'wrooom';
}
}
$ php -l example.php
No syntax errors detected in example.php
$ php example.php
PHP Fatal error: Declaration of Tesla::drive(): string must
be compatible with Car::drive(): int in example.php
PHP has very few compile time errors...
... But it is getting better.
PHP RFC: Always generate fatal error for incompatible method signatures
class Car
{
public function drive(): void {}
}
function testdrive(Car $car) {
$car->fly();
}
$ php -l example.php
No syntax errors detected in example.php
$ php example.php
(Exit code: 0) // no errors found
class Car
{
public function drive(): void {}
}
function testdrive(Car $car) {
$car->fly();
}
testdrive(new Car()); // execute above code
$ php example.php
PHP Fatal error: Uncaught Error: Call to undefined method Car::fly()
in example.php:9
class Car
{
public function drive(): void {}
}
function testdrive(Car $car, bool $fly) {
if ($fly) {
$car->fly();
} else {
$car->drive();
}
}
testdrive(new Car(), false); // works just fine
testdrive(new Car(), true); // kaboom
Predict the behavior of software without running it.
Promote runtime errors to compile time errors.
Find (a class of) bugs in your code without writing Tests!
Inferring types when types are not given.
function addStrings($part1, $part2)
{
// concat operation always yields a string
return $part1 . $part2;
}
// $foo can be inferred as string
$foo = addStrings($unknownA, $unknownB);
Type-specifying language constructs and functions
if (is_int($variable)) {
// here we can be sure that $variable is integer
}
/**
* @param DateTimeInterface $dateTime
* @param int $days
* @return \DateTimeInterface
*/
function addDays($dateTime, $days) {
[...]
}
function addDays(
\DateTimeInterface $dateTime,
int $days
): \DateTimeInterface {
[...]
}
/**
* @return string[]
*/
function getDaysOfWeek(): array {
return [
'monday',
'tuesday',
[...]
];
}
/**
* @return array<string,int>
*/
function getDayOfWeekMap(): array {
return [
'monday' => 1
'tuesday' => 2,
[...]
];
}
/**
* @return callable(int): int
*/
function createAdder(int $x): callable {
return function(int $y) use ($x): int {
return $x + $y;
};
}
$addFive = createAdder(5);
$addFive(10);
// output: int(15)
/**
* @param callable(string): string $transformer
*/
function greet(string $input, callable $transformer): string {
return 'Hello ' . $transformer($input);
}
$toUpperCase = function (string $x): string {
return strtoupper($x);
};
greet('hello symfony live', $toUpperCase);
// string(32) "HELLO SYMFONY LIVE"
/**
* @method Booking|null findOneByUuid(string $uuid)
* @method Booking[] findByUuid(string $uuid)
*/
class BookingRepository extends EntityRepository
{
public function __call($method, $arguments) {}
}
/** @var \DateTimeInterface $date */
$date = Thirdparty::createDate();
$date = Thirdparty::createDate();
assert($date instanceof \DateTimeInterface);
via: https://github.com/doctrine/coding-standard/pull/47
/**
* @return int|false
*/
function stripos ($haystack, $needle) {}
/** @var string|array $rabbit */
$rabbit = rand(0, 10) === 4 ? 'rabbit' : ['rabbit'];
/** @var CodeFormatter&\PHPUnit\MockObject */
$formatter = $this->createMock(CodeFormatter::class);
$formatter->expect(...); // method from MockObject
$formatter->format(...); // method from CodeFormatter
Some tools can tell you how discoverable your codebase is
Checks took 3.48 seconds and used 604.080MB of memory
Psalm was able to infer types for 95.3959% of the codebase
PHP-CS-Fixer and PHP_CodeSniffer can assist you with that
composer require --dev doctrine/coding-standard
function getDaysOfWeek(): array {
return [
'monday',
'tuesday',
[...]
];
}
$ ./vendor/bin/phpcs src/CodeStyle.php
FILE: /tmp/src/CodeStyle.php
-----------------------------------------------------------------
FOUND 1 ERROR AFFECTING 1 LINE
-----------------------------------------------------------------
9 | ERROR | Method \Examples\CodeStyle::getDaysOfWeek() does not
| | have @return annotation for its traversable return
| | value.
-----------------------------------------------------------------
function publishPost(
int $userId,
int $postId,
bool $isActive,
bool $isDraft // added later on
)
$postId = (int)$request->get('post_id');
$userId = (int)$request->get('user_id');
// many lines of code ...
publishPost(
$postId,
$userId, // doh!
true,
true // is that even valid?
);
Introduce Value Objects and Enums
function publishPost(
UserId $userId,
PostId $postId,
PostStatusEnum $status
)
$postId = PostId::fromInt((int)$request->get('post_id'));
$userId = UserId::fromInt((int)$request->get('user_id'));
// many lines of code ...
publishPost(
$postId,
$userId, // can be detected via SCA
PostStatusEnum::DRAFT()
);
Plain old PHP objects (POPO)
final class UserId
{
private $userId;
public static function fromInt(int $userId) : self
{
return new self($userId);
}
private function __construct(int $userId)
{
// one test case less for the consuming code
Assert::greaterThan($userId, 0);
$this->userId = $userId;
}
public function toInt() : int
{
return $this->userId;
}
}
use MyCLabs\Enum\Enum;
class PostStatusEnum extends Enum
{
private const DRAFT = 'draft';
private const PUBLISHED = 'active';
private const HIDDEN = 'hidden';
}
// own Date ValueObject with reduced precision
$startDate = Date::fromString('2019-05-04');
$endDate = Date::fromString('2019-05-06')
$endDate->equals($startDate); // false
$endDate->isLaterThan($startDate); // true
$startDate->addDays(5); // return new Date Object
Stay::fromDates(
$startDate,
$endDate,
);
// ensures bussiness restrictions
// throws exception: 'Start Date (%s) must be before end date (%s).',
$stay = Stay::fromStrings('2019-05-04', '2019-05-06');
$stay->getNumberOfNights(); // 2
A talk by Gary Bernhardt from Strange Loop 2015
destroyallsoftware.com/talks/ideology
nikic/php-parser by Nikita Popov
A PHP parser written in PHP
nikic/php-ast by Nikita Popov
Extension that exposes the abstract syntax tree generated by PHP 7
composer require nikic/php-parser
./vendor/bin/php-parse src/ast_example.php
Not very CI friendly
There is a inspection.sh for headless mode
vimeo/psalm | phan/phan | phpstan/phpstan | |
---|---|---|---|
Author | Vimeo | Rasmus Lerdorf | Ondřej Mirtes |
PHP Version | >= 5.6 | >= 7.0 | >= 7.1 |
AST | PHP-Parser | ext-ast | PHP-Parser |
And so much more: https://github.com/exakat/php-static-analysis-tools
composer require --dev phpstan/phpstan
./vendor/bin/phpstan analyse --level=2 src/
namespace Example;
class Testdriver
{
public static function testdrive(Car $car, bool $fly)
{
if ($fly) {
$car->fly();
} else {
$car->drive();
}
}
}
$ ./vendor/bin/phpstan analyse --level=2 src/Testdriver.php
ignoreErrors:
- '#Cannot call method getResult() on array|object|string.#'
composer require --dev vimeo/psalm
./vendor/bin/psalm --init src/ 8
./vendor/bin/psalm
./vendor/bin/psalm src/Testdriver.php
Scanning files...
Analyzing files...
ERROR: UndefinedMethod - src/Testdriver.php:12:19 -
Method Example\Car::fly does not exist
$car->fly();
------------------------------
1 errors found
------------------------------
Checks took 0.14 seconds and used 42.155MB of memory
Psalm was able to infer types for 96.2963% of the codebase
/**
* @param 'a'|'b' $s
* @psalm-param 'a'|'b' $s
*/
function foo(string $s) : string {
switch ($s) {
case 'a':
return 'hello';
case 'b':
return 'goodbye';
}
}
/**
* @param class-string $s
*/
function takesClassName(string $s) : void {}
return [
"foo" => 'fasel'
"bar" => false
];
/** @return array{foo: string, bar: bool} */
./vendor/bin/psalm --set-baseline=your-baseline.xml
./vendor/bin/psalm --update-baseline
psalm.dev/docs/manipulating_code/fixing/
./vendor/bin/psalter --issues=all --dry-run --safe-types
@psalm-readonly (for properties)
@psalm-pure
@psalm-mutation-free
@psalm-external-mutation-free
@psalm-immutable (for classes)
Give phpstan and/or psalm a try today
SCA is a tool to help you, not to hinder you
Please leave feedback
joind.in/talk/a97b6 @benjamincremer talks.benjamin-cremer.de/symfony_live_sca_2019