├── .editorconfig
├── .gitignore
├── LICENSE.md
├── README.md
├── bin
└── haigha
├── composer.json
├── examples
└── random_users.yml
├── phpunit.xml.dist
├── src
├── Command
│ └── LoadCommand.php
├── Exception
│ ├── FileNotFoundException.php
│ └── RuntimeException.php
├── Persister
│ └── PdoPersister.php
├── TableRecord.php
└── TableRecordInstantiator.php
└── tests
└── Persister
└── PdoPersisterTest.php
/.editorconfig:
--------------------------------------------------------------------------------
1 | root = true
2 |
3 | [*]
4 | charset = utf-8
5 | end_of_line = lf
6 | indent_style = space
7 | indent_size = 4
8 | insert_final_newline = true
9 | trim_trailing_whitespace = true
10 |
11 | [*.yml]
12 | indent_style = space
13 | indent_size = 2
14 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | *~
2 | vendor/
3 | composer.lock
4 | .DS_Store
5 | # Tests
6 | phpunit.xml
7 |
--------------------------------------------------------------------------------
/LICENSE.md:
--------------------------------------------------------------------------------
1 | # The MIT License
2 |
3 | Permission is hereby granted, free of charge, to any person obtaining a copy
4 | of this software and associated documentation files (the "Software"), to deal
5 | in the Software without restriction, including without limitation the rights
6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7 | copies of the Software, and to permit persons to whom the Software is
8 | furnished to do so, subject to the following conditions:
9 |
10 | The above copyright notice and this permission notice shall be included in
11 | all copies or substantial portions of the Software.
12 |
13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
19 | THE SOFTWARE.
20 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | > [!TIP]
2 | > Haigha has been superseeded by the brand new [Hatter](https://github.com/linkorb/haigha) - similar features, still application independant, but no longer trying to use Alice in a way that it wasn't designed for.
3 |
4 | # Haigha: Alice fixtures for tables
5 |
6 |
7 |
8 | [Alice](https://github.com/nelmio/alice) is an *awesome* database fixtures library.
9 | It works with Doctrine out-of-the-box, but if you don't use the Doctrine ORM, you'll need custom persisters...
10 |
11 | This is where **Haigha** comes in:
12 |
13 | > *Haigha lets you use Alice directly with database tables!*
14 |
15 | ## Features
16 |
17 | * Supports all standard Alice functionality (ranges, optional data, references, inheritence, etc)
18 | * Supports Faker data providers
19 | * Supports any PDO connection
20 | * No need to write classes, directly persist from yml to your sql database
21 |
22 | ## Example fixture file
23 |
24 | Haigha uses Alice to load fixture files, so the format is identical ([Details](https://github.com/nelmio/alice)). The only thing to keep in mind is that you use tablenames instead of classnames. Prefix your tablenames with `table.`. For example, if your tablename is called `user`, you use it like this:
25 |
26 | ```yaml
27 | table.group:
28 | group_random_users:
29 | id: 1 # This is important for version ~2.0
30 | name: Random users
31 |
32 | table.user:
33 | random_user{0..9}:
34 | group_id: @group_random_users
35 | username:
36 | firstname:
37 | lastname:
38 | password:
39 | email:
40 | ```
41 |
42 | ## How to use Haigha in your application
43 |
44 | Simply add the following to your `require` or `require-dev` section in your [composer.json](http://getcomposer.org) and run `composer update`:
45 |
46 | ```json
47 | "require": {
48 | "linkorb/haigha": "^2.0"
49 | }
50 | ```
51 |
52 | You can now use Haigha in your applications, or use the included command-line tool to load fixtures into your database:
53 |
54 | ## Command-line usage
55 |
56 | The haigha command-line tool knows one sub-command: `fixtures:load`.
57 |
58 | The first argument is the filename of your fixture definition (yaml).
59 |
60 | The second argument is the (optional) database url. If no database url is specified, haigha used your `PDO` environment variable instead.
61 |
62 | ### Database URL
63 |
64 | A full URL containing username, password, hostname and dbname. For example:
65 |
66 | ```
67 | ./vendor/bin/haigha fixtures:load examples/random_users.yml mysql://username:password@hostname/dbname
68 | ```
69 |
70 | ### Just a dbname
71 |
72 | In this case [linkorb/database-manager](https://github.com/linkorb/database-manager) is used for loading database connection details (server, username, password, etc) from .conf files (read project readme for more details).
73 |
74 | In a nutshell - you must have a `database_name.conf` file at `/share/config/database/` as described at [database-manager's documentation](https://github.com/linkorb/database-manager#database-configuration-files).
75 |
76 | ```bash
77 | ./vendor/bin/haigha fixtures:load examples/random_users.yml dbname
78 | ```
79 |
80 | ## Library usage:
81 |
82 | You can use Haigha in your own application like this:
83 |
84 | ```php
85 | // Instantiate a new Alice loader
86 | $loader = new Nelmio\Alice\Fixtures\Loader();
87 |
88 | // Add the Haigha instantiator
89 | $instantiator = new Haigha\TableRecordInstantiator();
90 | $loader->addInstantiator($instantiator);
91 |
92 | // Load (Haigha) objects from a Alice yml file
93 | $objects = $loader->load('examples/random_users.yml');
94 |
95 | // Instantiate the Haigha PDO persister, and pass a PDO connection
96 | $persister = new PdoPersister($pdo);
97 |
98 | // Persist the Haigha objects on the PDO connection
99 | $persister->persist($objects);
100 | ```
101 |
102 | ## Test
103 |
104 | Customize `phpunit.xml`:
105 |
106 | ```
107 | cp phpunit.xml.dist phpunit.xml
108 | ```
109 |
110 | Run:
111 |
112 | ```
113 | vendor/bin/phpunit
114 | ```
115 |
116 | ## License
117 |
118 | MIT (see [LICENSE.md](LICENSE.md))
119 |
120 | ## Brought to you by the LinkORB Engineering team
121 |
122 | 
123 | Check out our other projects at [linkorb.com/engineering](http://www.linkorb.com/engineering).
124 |
125 | Btw, we're hiring!
126 |
--------------------------------------------------------------------------------
/bin/haigha:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env php
2 | load($filename);
28 | }
29 |
30 | $application = new Application('Haigha', '1.1.0');
31 | $application->setCatchExceptions(true);
32 | $application->add(new \Haigha\Command\LoadCommand());
33 | $application->run();
34 |
--------------------------------------------------------------------------------
/composer.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "linkorb/haigha",
3 | "description": "Alice fixtures for database tables",
4 | "keywords": ["alice", "wonderland", "fixtures", "march hare"],
5 | "license": "MIT",
6 | "type": "library",
7 | "authors": [
8 | {
9 | "name": "Joost Faassen",
10 | "email": "j.faassen@linkorb.com",
11 | "homepage": "http://www.linkorb.com",
12 | "role": "Developer"
13 | }
14 | ],
15 | "support": {
16 | "issues": "https://github.com/linkorb/haigha/issues"
17 | },
18 | "require": {
19 | "php": ">=7.1",
20 | "symfony/dotenv": "^3.4|^4.3|^5.0",
21 | "ramsey/uuid": "^2.0|^3.0",
22 | "nelmio/alice": "^3.0",
23 | "symfony/console": "^3.4|^4.3|^5.0",
24 | "linkorb/database-manager": "^2.0"
25 | },
26 | "require-dev": {
27 | "phpunit/phpunit": "^5"
28 | },
29 | "bin": ["bin/haigha"],
30 | "autoload": {
31 | "psr-4": {
32 | "Haigha\\": "src/"
33 | }
34 | },
35 | "autoload-dev": {
36 | "psr-4": {
37 | "Haigha\\Tests\\": "tests/"
38 | }
39 | }
40 | }
41 |
--------------------------------------------------------------------------------
/examples/random_users.yml:
--------------------------------------------------------------------------------
1 | table.user:
2 | random_user{0..9}:
3 | username:
4 | firstname:
5 | lastname:
6 | password:
7 | email:
8 |
--------------------------------------------------------------------------------
/phpunit.xml.dist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
10 |
11 |
12 | tests/
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 | src/
23 |
24 |
25 |
26 |
--------------------------------------------------------------------------------
/src/Command/LoadCommand.php:
--------------------------------------------------------------------------------
1 | setName('fixtures:load')
28 | ->setDescription('Load Alice fixture data into database')
29 | ->addArgument(
30 | 'filename',
31 | InputArgument::REQUIRED,
32 | 'Filename'
33 | )
34 | ->addArgument(
35 | 'url',
36 | InputArgument::OPTIONAL,
37 | 'Database connection url'
38 | )
39 | ->addOption(
40 | 'dry-run',
41 | 'd',
42 | InputOption::VALUE_NONE,
43 | 'Do not run any SQL query - just pass to output',
44 | null
45 | )
46 | ->addArgument(
47 | 'autouuidfield',
48 | InputArgument::OPTIONAL,
49 | 'Fieldname for automatically generating uuids on all records'
50 | )
51 | ->addOption(
52 | 'append',
53 | 'a',
54 | InputOption::VALUE_NONE,
55 | 'Do not reset DB schema before loading fixtures',
56 | null
57 | )
58 | ->addOption(
59 | 'locale',
60 | 'l',
61 | InputOption::VALUE_REQUIRED,
62 | 'Locale for Alice',
63 | 'en_US'
64 | )
65 | ->addOption(
66 | 'seed',
67 | null,
68 | InputOption::VALUE_REQUIRED,
69 | 'Seed for Alice',
70 | 1
71 | )
72 | ;
73 | }
74 |
75 | /**
76 | * {@inheritdoc}
77 | */
78 | public function execute(InputInterface $input, OutputInterface $output)
79 | {
80 | $dburl = $input->getArgument('url');
81 | if (!$dburl) {
82 | $dburl = getenv('PDO');
83 | }
84 | if (!$dburl) {
85 | throw new RuntimeException("Database URL unspecified. Either pass as an argument, or configure your PDO environment variable.");
86 | }
87 | $filename = $input->getArgument('filename');
88 | $autoUuidField = $input->getArgument('autouuidfield');
89 | $locale = $input->getOption('locale');
90 | $seed = $input->getOption('seed');
91 |
92 | if (!file_exists($filename)) {
93 | throw new FileNotFoundException($filename);
94 | }
95 |
96 | $manager = new DatabaseManager();
97 | $pdo = $manager->getPdo($dburl, 'default');
98 |
99 | $providers = array();
100 | $loader = new AliceLoader($locale, $providers, $seed);
101 |
102 | $instantiator = new TableRecordInstantiator();
103 | if ($autoUuidField) {
104 | $instantiator->setAutoUuidColumn($autoUuidField);
105 | }
106 | $loader->addInstantiator($instantiator);
107 |
108 | $output->writeln(sprintf(
109 | "Loading '%s' into %s",
110 | $filename,
111 | $dburl
112 | ));
113 | $objects = $loader->load($filename);
114 |
115 | $output->writeln(sprintf(
116 | "Persisting '%s' objects in database '%s'",
117 | count($objects),
118 | $dburl
119 | ));
120 |
121 | $persister = new PdoPersister($pdo, $output, $input->getOption('dry-run'));
122 | if (!is_null($input->getOption('append'))) {
123 | $persister->reset($objects);
124 | }
125 | $persister->persist($objects);
126 |
127 | $output->writeln("Done");
128 | }
129 | }
130 |
--------------------------------------------------------------------------------
/src/Exception/FileNotFoundException.php:
--------------------------------------------------------------------------------
1 |
7 | */
8 | class FileNotFoundException extends RuntimeException
9 | {
10 | /**
11 | * @param string $path
12 | */
13 | public function __construct($path)
14 | {
15 | parent::__construct(sprintf(
16 | "File '%s' doesn't exists.",
17 | $path
18 | ));
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/src/Exception/RuntimeException.php:
--------------------------------------------------------------------------------
1 |
7 | */
8 | class RuntimeException extends \RuntimeException
9 | {
10 | }
11 |
--------------------------------------------------------------------------------
/src/Persister/PdoPersister.php:
--------------------------------------------------------------------------------
1 | pdo = $pdo;
22 | $this->output = $output;
23 | $this->dryRun = $dryRun;
24 | }
25 |
26 | /**
27 | * {@inheritdoc}
28 | */
29 | public function reset($objects)
30 | {
31 | $truncated = array();
32 | foreach ($objects as $object) {
33 | $tablename = $object->__meta('tablename');
34 | if (in_array($tablename, $truncated, true)) {
35 | continue;
36 | }
37 | $truncated[] = $tablename;
38 |
39 | $sql = sprintf("TRUNCATE `%s`", $tablename);
40 |
41 | if ($this->dryRun) {
42 | $this->output->writeln(sprintf("Will be executed: %s", $sql));
43 | continue;
44 | }
45 |
46 | $this->output->writeln(sprintf("Executing: %s", $sql));
47 | $statement = $this->pdo->prepare($sql);
48 | $statement->execute();
49 | }
50 | }
51 |
52 | /**
53 | * {@inheritdoc}
54 | */
55 | public function persist(array $objects)
56 | {
57 | $tasks = array();
58 | $count = 0;
59 |
60 | // collect all records
61 | foreach ($objects as $object) {
62 | $tablename = $object->__meta('tablename');
63 | $fields = get_object_vars($object);
64 |
65 | if (!isset($tasks[$tablename])) {
66 | $tasks[$tablename] = array(
67 | 'fields' => $this->implodeFieldsNames($fields),
68 | 'rawfields' => $fields,
69 | 'sql' => array(),
70 | 'params' => array(),
71 | );
72 | }
73 |
74 | $sql = array();
75 | $params = array();
76 | // insert known fields in the right order
77 | foreach ($tasks[$tablename]['rawfields'] as $field => $dummy) {
78 | if (isset($fields[$field])) {
79 | $params[$field.$count] = $fields[$field];
80 | $sql[] = ':'.$field.$count;
81 | } else {
82 | $sql[] = 'DEFAULT';
83 | }
84 | }
85 |
86 | // add newly found fields for this table if any
87 | foreach (array_diff_key($fields, $tasks[$tablename]['rawfields']) as $newKey => $value) {
88 | $params[$newKey.$count] = $value;
89 | $sql[] = ':' . $newKey.$count;
90 |
91 | // add DEFAULT value for the new fields to the previous records
92 | foreach ($tasks[$tablename]['sql'] as $index => $dummy) {
93 | $tasks[$tablename]['sql'][$index] = substr($tasks[$tablename]['sql'][$index], 0, -1) . ', DEFAULT)';
94 | }
95 |
96 | // define the new field in the known ones
97 | $tasks[$tablename]['fields'] .= ', `' . $newKey . '`';
98 | $tasks[$tablename]['rawfields'][$newKey] = true;
99 | }
100 | $count++;
101 |
102 | $tasks[$tablename]['sql'][] = '('.implode(', ', $sql).')';
103 | $tasks[$tablename]['params'] = array_merge($tasks[$tablename]['params'], $params);
104 | }
105 |
106 | // insert records
107 | $this->pdo->beginTransaction();
108 | try {
109 | foreach ($tasks as $table => $task) {
110 | $sql = 'INSERT INTO `' . $table . '` (' . $task['fields'] . ') VALUES ' . implode(",\n", $task['sql']);
111 | $params = $task['params'];
112 |
113 | if ($this->dryRun) {
114 | $this->output->writeln(sprintf(
115 | "Will be executed: %s",
116 | $this->getExpectedSqlQuery($sql, $params)
117 | ));
118 | continue;
119 | }
120 |
121 | $this->output->writeln(sprintf(
122 | "Executing: %s",
123 | $this->getExpectedSqlQuery($sql, $params)
124 | ));
125 |
126 | $statement = $this->pdo->prepare($sql);
127 | $res = $statement->execute($params);
128 |
129 | if (!$res) {
130 | $err = $statement->errorInfo();
131 | throw new RuntimeException(sprintf(
132 | "Error: '%s' on query '%s'",
133 | $err[2],
134 | $sql
135 | ));
136 | }
137 | }
138 | } catch (\Exception $e) {
139 | $this->pdo->rollBack();
140 | throw $e;
141 | }
142 | $this->pdo->commit();
143 | }
144 |
145 | /**
146 | * {@inheritdoc}
147 | */
148 | public function find($class, $id)
149 | {
150 | throw new RuntimeException('find not implemented');
151 | }
152 |
153 | /**
154 | * @param array $fields
155 | * @return string
156 | */
157 | private function implodeFieldsNames($fields)
158 | {
159 | $fields_names = array_keys($fields);
160 | return "`" . implode("`, `", $fields_names) . "`";
161 | }
162 |
163 | public function getExpectedSqlQuery($sql, $fields)
164 | {
165 | foreach ($fields as $key=>$value) {
166 | $key = preg_quote($key);
167 | $sql = preg_replace("/:$key/", $value, $sql);
168 | }
169 | return $sql;
170 | }
171 | }
172 |
--------------------------------------------------------------------------------
/src/TableRecord.php:
--------------------------------------------------------------------------------
1 | __meta['tablename'] = $tablename;
17 | }
18 |
19 | /**
20 | * @todo Determine primary field name
21 | *
22 | * @return string ID
23 | */
24 | public function __toString()
25 | {
26 | try {
27 | return (string)$this->id;
28 | } catch (Exception $exception) {
29 | return '';
30 | }
31 | }
32 |
33 | public function __call($key, $params)
34 | {
35 | if (substr($key, 0, 3) == 'set') {
36 | $var = lcfirst(substr($key, 3));
37 | $value = $params[0];
38 | if ($value instanceof \DateTime) {
39 | $value = $value->getTimeStamp();
40 | }
41 | $this->$var = $value;
42 | } else {
43 | throw new RuntimeException(sprintf(
44 | "Unexpected key passed to magic call to TableRecord: '%s'",
45 | $key
46 | ));
47 | }
48 | }
49 |
50 | public function __meta($key)
51 | {
52 | return $this->__meta[$key];
53 | }
54 | }
55 |
--------------------------------------------------------------------------------
/src/TableRecordInstantiator.php:
--------------------------------------------------------------------------------
1 | auto_uuid_column = $colum_name;
21 | }
22 |
23 | /**
24 | * {@inheritDoc}
25 | */
26 | public function canInstantiate(Fixture $fixture)
27 | {
28 | if (substr($fixture->getClass(), 0, 6)=='table.') {
29 | return true;
30 | }
31 | }
32 |
33 | /**
34 | * {@inheritDoc}
35 | */
36 | public function instantiate(Fixture $fixture)
37 | {
38 | $tablename = substr($fixture->getClass(), 6);
39 | $r = new TableRecord($tablename);
40 |
41 | if ($this->auto_uuid_column) {
42 | $uuid = (string)Uuid::uuid4();
43 | $r->setR_uuid($uuid);
44 | }
45 |
46 | return $r;
47 | }
48 | }
49 |
--------------------------------------------------------------------------------
/tests/Persister/PdoPersisterTest.php:
--------------------------------------------------------------------------------
1 |
13 | */
14 | class PdoPersisterTest extends \PHPUnit_Framework_TestCase
15 | {
16 | private $persister;
17 | private $output;
18 |
19 | public function setUp()
20 | {
21 | $pdo = $this->getMockBuilder('PDO')
22 | ->disableOriginalConstructor()
23 | ->getMock()
24 | ;
25 | $this->output = new BufferedOutput();
26 | $this->persister = new PdoPersister($pdo, $this->output, true);
27 | }
28 |
29 | public function testPersist()
30 | {
31 | $records = array(
32 | $this->makeRecord('table1', array('a' => 'foo')),
33 | $this->makeRecord('table1', array('b' => 'bar')),
34 | $this->makeRecord('table1', array('b' => 'baz', 'a' => 'qux')),
35 | $this->makeRecord('table2', array('x' => 'y')),
36 | );
37 |
38 | $this->persister->persist($records);
39 | $expected = "Will be executed: INSERT INTO `table1` (`a`, `b`) VALUES (foo, DEFAULT),\n".
40 | "(DEFAULT, bar),\n".
41 | "(qux, baz)\n".
42 | "Will be executed: INSERT INTO `table2` (`x`) VALUES (y)\n";
43 |
44 | $this->assertEquals($expected, $this->output->fetch());
45 | }
46 |
47 | public function testReset()
48 | {
49 | $records = array(
50 | $this->makeRecord('table1', array('a' => 'foo')),
51 | $this->makeRecord('table1', array('b' => 'bar')),
52 | $this->makeRecord('table2', array('x' => 'y')),
53 | );
54 |
55 | $this->persister->reset($records);
56 | $expected = "Will be executed: TRUNCATE `table1`\n".
57 | "Will be executed: TRUNCATE `table2`\n";
58 |
59 | $this->assertEquals($expected, $this->output->fetch());
60 | }
61 |
62 | private function makeRecord($table, $fields)
63 | {
64 | $record = new TableRecord($table);
65 | foreach ($fields as $field => $val) {
66 | $record->{$field} = $val;
67 | }
68 |
69 | return $record;
70 | }
71 | }
72 |
--------------------------------------------------------------------------------