├── .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 | --------------------------------------------------------------------------------