packages/sql-fixture/src/Provider/FixtureProvider.php

1<?php
2
3declare(strict_types=1);
4
5namespace SqlFixture\Provider;
6
7use Faker\Generator;
8use SqlFixture\Fixture;
9use SqlFixture\Fixture\FixtureSet;
10use SqlFixture\Fixture\PlanGenerator;
11use SqlFixture\Fixture\TableOverrides;
12use SqlFixture\Hydrator\HydratorInterface;
13use SqlFixture\Plan\FixturePlan;
14use SqlFixture\Provider;
15use SqlFixture\Schema\SchemaParserInterface;
16use SqlFixture\Schema\StaticSchemaResolver;
17use SqlFixture\Schema\TableSchema;
18use SqlFixture\TypeMapper\TypeMapperInterface;
19use SqlFixture\Version\ServerVersion;
20
21/**
22 * Faker provider that generates fixtures from CREATE TABLE SQL statements.
23 *
24 * The dialect and, optionally, the version tag select the release the
25 * statements are read for; omitting the version tag uses the default of the
26 * dialect.
27 *
28 * @visibility public
29 * @example Read statements as a MySQL 8.0 server does
30 *     $provider = new \SqlFixture\Provider\FixtureProvider(\Faker\Factory::create(), dialect: 'mysql', version: 'mysql-8.0.44');
31 *     $provider->getVersion() // => 'mysql-8.0.44'
32 * @example Generate a row while leaving the auto-increment key to the database
33 *     $provider = new \SqlFixture\Provider\FixtureProvider(\Faker\Factory::create());
34 *     $provider->fixture('CREATE TABLE users (id INT AUTO_INCREMENT PRIMARY KEY, name VARCHAR(30))', ['name' => 'Alice']) // => ['name' => 'Alice']
35 *
36 * @example Generate linked rows from registered schemas
37 *     $provider = new \SqlFixture\Provider\FixtureProvider(\Faker\Factory::create());
38 *     $provider->registerSchema('CREATE TABLE users (id INT PRIMARY KEY)');
39 *     $provider->registerSchema('CREATE TABLE posts (user_id INT)');
40 *     $rows = $provider->fixtures('users.id < posts.user_id', ['users' => ['id' => 9], 'posts' => 2]);
41 *     $rows->rows('posts') // => [['user_id' => 9], ['user_id' => 9]]
42 */
43class FixtureProvider extends SqlSchemaProvider
44{
45    private FixtureGenerator $fixtureGenerator;
46    private string $dialect;
47    private ServerVersion $version;
48    private Generator $faker;
49    private StaticSchemaResolver $schemaResolver;
50
51    /**
52     * @param string $dialect SQL dialect: 'mysql', 'pgsql' or 'sqlite'
53     * @param string|null $version Version tag such as 'mysql-8.4.7', or null for the default of the dialect
54     *
55     * @throws Exception\UnsupportedDriverException If the dialect is not supported
56     * @throws \SqlFixture\Version\UnsupportedVersionException If the version tag is not a supported release of the dialect
57     */
58    public function __construct(
59        Generator $faker,
60        ?TypeMapperInterface $typeMapper = null,
61        ?HydratorInterface $hydrator = null,
62        ?SchemaParserInterface $schemaParser = null,
63        string $dialect = PlatformFactory::DRIVER_MYSQL,
64        ?string $version = null,
65    ) {
66        parent::__construct($faker);
67        $this->faker = $faker;
68        $this->dialect = $dialect;
69        $this->version = PlatformFactory::resolveVersion($dialect, $version);
70        $this->schemaResolver = new StaticSchemaResolver();
71
72        $typeMapper ??= PlatformFactory::createTypeMapper($dialect);
73        $schemaParser ??= PlatformFactory::createSchemaParser($dialect, $this->version->tag);
74
75        $this->fixtureGenerator = new FixtureGenerator($faker, $typeMapper, $hydrator, $schemaParser);
76    }
77
78    /**
79     * Generate a fixture from a CREATE TABLE SQL statement.
80     *
81     * @template T of object
82     * @param string $createTableSql CREATE TABLE SQL statement
83     * @param array<string, mixed> $overrides Override values
84     * @param class-string<T>|null $className Deserialization target class
85     * @param string|null $dialect SQL dialect for this specific call (overrides constructor default)
86     * @param string|null $version Version tag for this specific call (overrides constructor default; the default of the dialect when only the dialect is overridden)
87     * @return ($className is null ? array<string, mixed> : T)
88     */
89    public function fixture(
90        string $createTableSql,
91        array $overrides = [],
92        ?string $className = null,
93        ?string $dialect = null,
94        ?string $version = null,
95    ): array|object {
96        $schema = $this->getSchema($createTableSql, $dialect, $version);
97        return $this->fixtureGenerator->generate($schema, $overrides, $className);
98    }
99
100    /**
101     * Generate the rows a plan describes.
102     *
103     * Every table the plan names has to have been registered, either through
104     * registerSchema() or by generating a fixture from its DDL first.
105     *
106     * @param FixturePlan|string $plan A plan, or the relation syntax for one
107     * @param array<string, int|array<mixed>|TableOverrides> $overrides Table name => what to override
108     */
109    public function fixtures(FixturePlan|string $plan, array $overrides = []): FixtureSet
110    {
111        $generator = new PlanGenerator($this->schemaResolver, $this->fixtureGenerator, $this->faker);
112
113        return $generator->generate(FixturePlan::from($plan), $overrides);
114    }
115
116    /**
117     * Make a table available to fixtures() under its own name.
118     */
119    public function registerSchema(string $createTableSql): TableSchema
120    {
121        return $this->getSchema($createTableSql);
122    }
123
124    /**
125     * The registry backing fixtures().
126     */
127    public function getSchemaResolver(): StaticSchemaResolver
128    {
129        return $this->schemaResolver;
130    }
131
132    /**
133     * Get the underlying fixture generator.
134     */
135    public function getFixtureGenerator(): FixtureGenerator
136    {
137        return $this->fixtureGenerator;
138    }
139
140    /**
141     * Get the default dialect.
142     */
143    public function getDialect(): string
144    {
145        return $this->dialect;
146    }
147
148    /**
149     * Get the version tag of the release statements are read for.
150     */
151    public function getVersion(): string
152    {
153        return $this->version->tag;
154    }
155}
156