1<?php
2/*
3 *  $Id$
4 *
5 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
6 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
7 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
8 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
9 * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
10 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
11 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
12 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
13 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
14 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
15 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
16 *
17 * This software consists of voluntary contributions made by many individuals
18 * and is licensed under the LGPL. For more information, see
19 * <http://www.doctrine-project.org>.
20 */
21
22/**
23 * Doctrine_Record_Generator
24 *
25 * @author      Konsta Vesterinen <kvesteri@cc.hut.fi>
26 * @package     Doctrine
27 * @subpackage  Plugin
28 * @license     http://www.opensource.org/licenses/lgpl-license.php LGPL
29 * @version     $Revision$
30 * @link        www.doctrine-project.org
31 * @since       1.0
32 */
33abstract class Doctrine_Record_Generator extends Doctrine_Record_Abstract
34{
35    /**
36     * _options
37     *
38     * @var array $_options     an array of plugin specific options
39     */
40    protected $_options = array(
41        'generateFiles'  => false,
42        'generatePath'   => false,
43        'builderOptions' => array(),
44        'identifier'     => false,
45        'table'          => false,
46        'pluginTable'    => false,
47        'children'       => array(),
48        'cascadeDelete'  => true,
49        'appLevelDelete' => false
50    );
51
52    /**
53     * Whether or not the generator has been initialized
54     *
55     * @var bool $_initialized
56     */
57    protected $_initialized = false;
58
59    /**
60     * An alias for getOption
61     *
62     * @param string $option
63     */
64    public function __get($option)
65    {
66        if (isset($this->_options[$option])) {
67            return $this->_options[$option];
68        }
69        return null;
70    }
71
72    /**
73     * __isset
74     *
75     * @param string $option
76     */
77    public function __isset($option)
78    {
79        return isset($this->_options[$option]);
80    }
81
82    /**
83     * Returns the value of an option
84     *
85     * @param $option       the name of the option to retrieve
86     * @return mixed        the value of the option
87     */
88    public function getOption($name)
89    {
90        if ( ! isset($this->_options[$name])) {
91            throw new Doctrine_Exception('Unknown option ' . $name);
92        }
93
94        return $this->_options[$name];
95    }
96
97    /**
98     * Sets given value to an option
99     *
100     * @param $option       the name of the option to be changed
101     * @param $value        the value of the option
102     * @return Doctrine_Plugin  this object
103     */
104    public function setOption($name, $value)
105    {
106        $this->_options[$name] = $value;
107
108        return $this;
109    }
110
111    /**
112     * Add child record generator
113     *
114     * @param  Doctrine_Record_Generator $generator
115     * @return void
116     */
117    public function addChild($generator)
118    {
119        $this->_options['children'][] = $generator;
120    }
121
122    /**
123     * Returns all options and their associated values
124     *
125     * @return array    all options as an associative array
126     */
127    public function getOptions()
128    {
129        return $this->_options;
130    }
131
132    /**
133     * Initialize the plugin. Call in Doctrine_Template setTableDefinition() in order to initiate a generator in a template
134     *
135     * @see Doctrine_Template_I18n
136     * @param  Doctrine_Table $table
137     * @return void
138     */
139    public function initialize(Doctrine_Table $table)
140    {
141      	if ($this->_initialized) {
142      	    return false;
143      	}
144
145        $this->_initialized = true;
146
147        $this->initOptions();
148
149        $table->addGenerator($this, get_class($this));
150
151        $this->_options['table'] = $table;
152
153        $ownerClassName = $this->_options['table']->getComponentName();
154        $className = $this->_options['className'];
155        $this->_options['className'] = str_replace('%CLASS%', $ownerClassName, $className);
156
157        if (isset($this->_options['tableName'])) {
158            $ownerTableName = $this->_options['table']->getTableName();
159            $tableName = $this->_options['tableName'];
160            $this->_options['tableName'] = str_replace('%TABLE%', $ownerTableName, $tableName);
161        }
162
163        // check that class doesn't exist (otherwise we cannot create it)
164        if ($this->_options['generateFiles'] === false && class_exists($this->_options['className'])) {
165            $this->_table = Doctrine_Core::getTable($this->_options['className']);
166            return false;
167        }
168
169        $this->buildTable();
170
171        $fk = $this->buildForeignKeys($this->_options['table']);
172
173        $this->_table->setColumns($fk);
174
175        $this->buildRelation();
176
177        $this->setTableDefinition();
178        $this->setUp();
179
180        $this->generateClassFromTable($this->_table);
181
182        $this->buildChildDefinitions();
183
184        $this->_table->initIdentifier();
185    }
186
187    /**
188     * Create the new Doctrine_Table instance in $this->_table based on the owning
189     * table.
190     *
191     * @return void
192     */
193    public function buildTable()
194    {
195        // Bind model
196        $conn = $this->_options['table']->getConnection();
197        $bindConnName = $conn->getManager()->getConnectionForComponent($this->_options['table']->getComponentName())->getName();
198        if ($bindConnName) {
199            $conn->getManager()->bindComponent($this->_options['className'], $bindConnName);
200        } else {
201            $conn->getManager()->bindComponent($this->_options['className'], $conn->getName());
202        }
203
204        // Create table
205        $tableClass = $conn->getAttribute(Doctrine_Core::ATTR_TABLE_CLASS);
206        $this->_table = new $tableClass($this->_options['className'], $conn);
207        $this->_table->setGenerator($this);
208
209        // If custom table name set then lets use it
210        if (isset($this->_options['tableName']) && $this->_options['tableName']) {
211            $this->_table->setTableName($this->_options['tableName']);
212        }
213
214        // Maintain some options from the parent table
215        $options = $this->_options['table']->getOptions();
216
217        $newOptions = array();
218        $maintain = array('type', 'collate', 'charset'); // This list may need updating
219        foreach ($maintain as $key) {
220            if (isset($options[$key])) {
221                $newOptions[$key] = $options[$key];
222            }
223        }
224
225        $this->_table->setOptions($newOptions);
226
227        $conn->addTable($this->_table);
228    }
229
230    /**
231     * Empty template method for providing the concrete plugins the ability
232     * to initialize options before the actual definition is being built
233     *
234     * @return void
235     */
236    public function initOptions()
237    {
238
239    }
240
241    /**
242     * Build the child behavior definitions that are attached to this generator
243     *
244     * @return void
245     */
246    public function buildChildDefinitions()
247    {
248        if ( ! isset($this->_options['children'])) {
249            throw new Doctrine_Record_Exception("Unknown option 'children'.");
250        }
251
252        foreach ($this->_options['children'] as $child) {
253            if ($child instanceof Doctrine_Template) {
254                if ($child->getPlugin() !== null) {
255                    $this->_table->addGenerator($child->getPlugin(), get_class($child->getPlugin()));
256                }
257
258                $this->_table->addTemplate(get_class($child), $child);
259
260                $child->setInvoker($this);
261                $child->setTable($this->_table);
262                $child->setTableDefinition();
263                $child->setUp();
264            } else {
265                $this->_table->addGenerator($child, get_class($child));
266                $child->initialize($this->_table);
267            }
268        }
269    }
270
271    /**
272     * Generates foreign keys for the plugin table based on the owner table.
273     * These columns are automatically added to the generated model so we can
274     * create foreign keys back to the table object that owns the plugin.
275     *
276     * @param Doctrine_Table $table     the table object that owns the plugin
277     * @return array                    an array of foreign key definitions
278     */
279    public function buildForeignKeys(Doctrine_Table $table)
280    {
281        $fk = array();
282
283        foreach ((array) $table->getIdentifier() as $field) {
284            $def = $table->getDefinitionOf($field);
285
286            unset($def['autoincrement']);
287            unset($def['sequence']);
288            unset($def['primary']);
289
290            $col = $table->hasColumn($field) ? $field : $table->getColumnName($field) . ' as ' . $field;
291
292            $def['primary'] = true;
293            $fk[$col] = $def;
294        }
295        return $fk;
296    }
297
298    /**
299     * Build the local relationship on the generated model for this generator
300     * instance which points to the invoking table in $this->_options['table']
301     *
302     * @param string $alias Alias of the foreign relation
303     * @return void
304     */
305    public function buildLocalRelation($alias = null)
306    {
307        $options = array(
308            'local'      => $this->getRelationLocalKey(),
309            'foreign'    => $this->getRelationForeignKey(),
310            'owningSide' => true
311        );
312
313        if (isset($this->_options['cascadeDelete']) && $this->_options['cascadeDelete'] && ! $this->_options['appLevelDelete']) {
314            $options['onDelete'] = 'CASCADE';
315            $options['onUpdate'] = 'CASCADE';
316        }
317
318        $aliasStr = '';
319
320        if ($alias !== null) {
321            $aliasStr = ' as ' . $alias;
322        }
323
324        $this->hasOne($this->_options['table']->getComponentName() . $aliasStr, $options);
325    }
326
327    /**
328     * Add a Doctrine_Relation::MANY relationship to the generator owner table
329     *
330     * @param string $name
331     * @param array $options
332     * @return void
333     */
334    public function ownerHasMany($name, $options)
335    {
336        $this->_options['table']->hasMany($name, $options);
337    }
338
339    /**
340     * Add a Doctrine_Relation::ONE relationship to the generator owner table
341     *
342     * @param string $name
343     * @param array $options
344     * @return void
345     */
346    public function ownerHasOne($name, $options)
347    {
348        $this->_options['table']->hasOne($name, $options);
349    }
350
351    /**
352     * Build the foreign relationship on the invoking table in $this->_options['table']
353     * which points back to the model generated in this generator instance.
354     *
355     * @param string $alias Alias of the foreign relation
356     * @return void
357     */
358    public function buildForeignRelation($alias = null)
359    {
360        $options = array(
361            'local'    => $this->getRelationForeignKey(),
362            'foreign'  => $this->getRelationLocalKey(),
363            'localKey' => false
364        );
365
366        if (isset($this->_options['cascadeDelete']) && $this->_options['cascadeDelete'] && $this->_options['appLevelDelete']) {
367            $options['cascade'] = array('delete');
368        }
369
370        $aliasStr = '';
371
372        if ($alias !== null) {
373            $aliasStr = ' as ' . $alias;
374        }
375
376        $this->ownerHasMany($this->_table->getComponentName() . $aliasStr, $options);
377    }
378
379    /**
380     * Get the local key of the generated relationship
381     *
382     * @return string $local
383     */
384    public function getRelationLocalKey()
385    {
386        return $this->getRelationForeignKey();
387    }
388
389    /**
390     * Get the foreign key of the generated relationship
391     *
392     * @return string $foreign
393     */
394    public function getRelationForeignKey()
395    {
396        $table = $this->_options['table'];
397        $identifier = $table->getIdentifier();
398
399        foreach ((array) $identifier as $column) {
400            $def = $table->getDefinitionOf($column);
401            if (isset($def['primary']) && $def['primary'] && isset($def['autoincrement']) && $def['autoincrement']) {
402                return $column;
403            }
404        }
405
406        return $identifier;
407    }
408
409    /**
410     * This method can be used for generating the relation from the plugin
411     * table to the owner table. By default buildForeignRelation() and buildLocalRelation() are called
412     * Those methods can be overridden or this entire method can be overridden
413     *
414     * @return void
415     */
416    public function buildRelation()
417    {
418        $this->buildForeignRelation();
419        $this->buildLocalRelation();
420    }
421
422    /**
423     * Generate a Doctrine_Record from a populated Doctrine_Table instance
424     *
425     * @param Doctrine_Table $table
426     * @return void
427     */
428    public function generateClassFromTable(Doctrine_Table $table)
429    {
430        $definition = array();
431        $definition['columns'] = $table->getColumns();
432        $definition['tableName'] = $table->getTableName();
433        $definition['actAs'] = $table->getTemplates();
434
435        return $this->generateClass($definition);
436    }
437
438    /**
439     * Generates the class definition for plugin class
440     *
441     * @param array $definition  Definition array defining columns, relations and options
442     *                           for the model
443     * @return void
444     */
445    public function generateClass(array $definition = array())
446    {
447        $definition['className'] = $this->_options['className'];
448        $definition['toString'] = isset($this->_options['toString']) ? $this->_options['toString'] : false;
449        if (isset($this->_options['listeners'])) {
450            $definition['listeners'] = $this->_options['listeners'];
451        }
452
453        $builder = new Doctrine_Import_Builder();
454        $builderOptions = isset($this->_options['builderOptions']) ? (array) $this->_options['builderOptions']:array();
455        $builder->setOptions($builderOptions);
456
457        if ($this->_options['generateFiles']) {
458            if (isset($this->_options['generatePath']) && $this->_options['generatePath']) {
459                $builder->setTargetPath($this->_options['generatePath']);
460                $builder->buildRecord($definition);
461            } else {
462                throw new Doctrine_Record_Exception('If you wish to generate files then you must specify the path to generate the files in.');
463            }
464        } else {
465            $def = $builder->buildDefinition($definition);
466
467            eval($def);
468        }
469    }
470}