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