Skip to content

Commit

Permalink
add simple MySQLi extension example
Browse files Browse the repository at this point in the history
  • Loading branch information
schlndh committed Jun 29, 2024
1 parent 26e2cef commit 5639fad
Show file tree
Hide file tree
Showing 7 changed files with 157 additions and 0 deletions.
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,8 @@ in your `phpstan.neon`.
You can use the MySQLi extension as a starting point a modify it to match your needs. The basic idea is to get the query
string from PHPStan, pass it to MariaStan for analysis and then report result types and errors back to PHPStan.

Before you start implementing your own extension to integrate MariaStan into your project, you can quickly try it out. You can start by checking out a [simple example](examples/MySQLi/README.md) which uses the MySQLi extension. Then you can try to call queries from your codebase via MySQLi and analyze them with the MySQLi extension to make sure that MariaStan supports the features which your projects uses.

## Features

Here is a list of features that you could implement into your own PHPStan extension based on MariaStan
Expand Down
18 changes: 18 additions & 0 deletions examples/MySQLi/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
This simple example showcases basic features of maria-stan via the built-in MySQLi PHPStan extension. See the example file [run.php](run.php) and PHPStan's output in [phpstan.out](phpstan.out). You can also try it out yourself:

First, clone the repository and run `composer install` in the root directory. Then set up a MariaDB instance. You can use the following docker command to run a new one (from root directory of the project):

```bash
docker run --detach --name maria-stan-example-mysqli \
--volume ./examples/MySQLi/data.sql:/docker-entrypoint-initdb.d/init.sql --env MARIADB_ROOT_PASSWORD=root --env MARIADB_DATABASE=maria-stan-example-mysqli \
--publish 13306:3306 --rm mariadb:10.11.5-jammy
```

Or you can use whatever MariaDB instance you already have running. However, you'll probably have to modify the `phpstan.neon`, because I'm using a non-standard MariaDB port to avoid conflict with other MariaDB instances already running on your system.

Finally, you can run phpstan and see the result for yourself (from root directory of the project):

`php vendor/bin/phpstan analyse -c examples/MySQLi/phpstan.neon --debug examples/MySQLi/run.php`

If you are curious about what else maria-stan can do, you can edit the `run.php` script and see what happens when you
try other queries (keep in mind that the MySQLi PHPStan extension is incomplete, so some use-cases may not be covered).
34 changes: 34 additions & 0 deletions examples/MySQLi/data.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
CREATE TABLE schools (
id INT NOT NULL PRIMARY KEY AUTO_INCREMENT,
name VARCHAR (255) NOT NULL,
type ENUM ('elementary', 'secondary', 'university') NOT NULL
);

INSERT INTO schools (id, name, type)
VALUES (1, 'Foo elementary school', 'elementary'), (2, 'Bar secondary school', 'secondary'), (3, 'Baz university', 'university');

CREATE TABLE people (
id INT NOT NULL PRIMARY KEY AUTO_INCREMENT,
name VARCHAR (255) NOT NULL
);

INSERT INTO people (id, name)
VALUES (1, 'John Doe'), ('2', 'Jane Doe'), (3, 'John Doe jr.'), (4, 'Jane Doe jr.');

CREATE TABLE classes (
id INT NOT NULL PRIMARY KEY AUTO_INCREMENT,
name VARCHAR(255) NOT NULL,
school_id INT NOT NULL REFERENCES schools (id),
teacher_id INT NULL REFERENCES people (id)
);

INSERT INTO classes (id, name, school_id, teacher_id)
VALUES (1, 'History', 1, 1), (2, 'Math', 1, 1), (3, 'PHP Programming', 2, 2), (4, 'Algorithms and Data Structures', 3, 2);

CREATE TABLE class_students (
class_id INT NOT NULL REFERENCES people (id),
student_id INT NOT NULL REFERENCES people (id)
);

INSERT INTO class_students (class_id, student_id)
VALUES (1, 1), (2, 1), (3, 2);
12 changes: 12 additions & 0 deletions examples/MySQLi/phpstan.neon
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
includes:
- ../../extension.mysqli.neon

parameters:
level: 9
maria-stan:
db:
host: 127.0.0.1
port: 13306
user: 'root'
password: 'root'
database: 'maria-stan-example-mysqli'
15 changes: 15 additions & 0 deletions examples/MySQLi/phpstan.out
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
------ ----------------------------------------------------------------------------------
Line run.php
------ ----------------------------------------------------------------------------------
28 Dumped type: array<int, array{id: int, name: string, type:
'elementary'|'secondary'|'university'}>
42 Dumped type: array<int, array{name: string, average_no_classes: numeric-string}>
55 Dumped type: array<int, array{id: int, name?: string, type?:
'elementary'|'secondary'|'university'}>
59 Table 'non_existent_table' doesn't exist
67 The used SELECT statements have a different number of columns: 3 vs 2.
------ ----------------------------------------------------------------------------------


[ERROR] Found 5 errors

75 changes: 75 additions & 0 deletions examples/MySQLi/run.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
<?php

declare(strict_types=1);

// @phpstan-ignore-next-line phpstanApi.phpstanNamespace
namespace PHPStan {

use function function_exists;
use function var_dump;

if (! function_exists('dumpType')) {
function dumpType(mixed $value): void
{
var_dump($value);
}
}
}

namespace {

use function rand;

// These credentials are not used for analysis. They are here just so you can try to run the script.
$db = new mysqli('127.0.0.1', 'root', 'root', 'maria-stan-example-mysqli', 13306);

// Trivial query - maria-stan should report the correct result type based on the table schema.
$result = $db->query('SELECT * FROM schools')->fetch_all(\MYSQLI_ASSOC);
\PHPStan\dumpType($result);

// A slightly more complicated query. Notice that maria-stan recognizes that average_no_classes cannot be null!
$result = $db->query('
SELECT s.name, AVG(number_of_classes) average_no_classes
FROM schools s
JOIN (
SELECT c.school_id, cs.student_id, COUNT(*) number_of_classes
FROM classes c
JOIN class_students cs ON c.id = cs.class_id
GROUP BY c.school_id, cs.student_id
) cs ON cs.school_id = s.id
GROUP BY s.id
')->fetch_all(\MYSQLI_ASSOC);
\PHPStan\dumpType($result);

// Sometimes the query can be partially dynamic. But as long as PHPStan is able to provide us with a union of
// constant strings, such queries can still be analysed. Keep in mind that PHPStan has a limit of how many constant
// values can be in a union, before it gets generalized. Each possible query is analysed individually,
// and the results (i.e. return types and errors) are then combined.
$table = rand()
? 'schools'
: 'people';
$col = rand()
? 'id'
: '*';
$result = $db->query("SELECT {$col} FROM {$table}" . ' WHERE 1')->fetch_all(\MYSQLI_ASSOC);
\PHPStan\dumpType($result);

// Here we have a trivially wrong query - it references a table which doesn't exist. Maria-stan should report it.
try {
$db->query('SELECT * FROM non_existent_table');
die('This was expected to fail!');
} catch (mysqli_sql_exception) {
}

// Here is a slightly more sneaky error. You can probably imagine that you have a query like this, which worked
// when it was first written, but then someone adds a column into one of the tables which breaks the query.
try {
$db->query('
SELECT * FROM schools
UNION ALL
SELECT * FROM people
');
die('This was expected to fail!');
} catch (mysqli_sql_exception) {
}
}
1 change: 1 addition & 0 deletions phpstan.neon.dist
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ parameters:
checkBenevolentUnionTypes: true
excludePaths:
- tests/*/data/*
- examples
exceptions:
check:
missingCheckedExceptionInThrows: true
Expand Down

0 comments on commit 5639fad

Please sign in to comment.