PageRenderTime 25ms CodeModel.GetById 10ms app.highlight 12ms RepoModel.GetById 1ms app.codeStats 0ms

/app/plugins/Db/libraries/Atomik/Model/Builder/ClassMetadata.php

https://bitbucket.org/fbertagnin/fbwork4
PHP | 360 lines | 205 code | 50 blank | 105 comment | 55 complexity | 13d39710b197e5767f9ff2add6e2bb06 MD5 | raw file
  1<?php
  2/**
  3 * Atomik Framework
  4 * Copyright (c) 2008-2009 Maxime Bouroumeau-Fuseau
  5 *
  6 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
  7 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
  8 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
  9 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 10 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 11 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 12 * THE SOFTWARE.
 13 *
 14 * @package Atomik
 15 * @subpackage Model
 16 * @author Maxime Bouroumeau-Fuseau
 17 * @copyright 2008-2009 (c) Maxime Bouroumeau-Fuseau
 18 * @license http://www.opensource.org/licenses/mit-license.php
 19 * @link http://www.atomikframework.com
 20 */
 21
 22/** Atomik_Model_Builder */
 23require_once 'Atomik/Model/Builder.php';
 24
 25/** Atomik_Model_Builder_Factory */
 26require_once 'Atomik/Model/Builder/Factory.php';
 27
 28/**
 29 * Reads the doc comments of a class and its properties and generates a model builder
 30 * 
 31 * @package Atomik
 32 * @subpackage Model
 33 */
 34class Atomik_Model_Builder_ClassMetadata
 35{
 36	/**
 37	 * @var array
 38	 */
 39	private static $_cache = array();
 40	
 41	/**
 42	 * Reads metadata from a class doc comments and creates a builder object
 43	 * 
 44	 * @param	string	$className
 45	 * @return 	Atomik_Model_Builder
 46	 */
 47	public static function read($className)
 48	{
 49		$builder = self::_getBaseBuilder($className);
 50		
 51		// extract references
 52		$references = array();
 53		if ($builder->hasOption('has')) {
 54			$references = (array) $builder->getOption('has');
 55			$builder->removeOption('has');
 56		}
 57		
 58		// adds references
 59		foreach ($references as $referenceString) {
 60			self::addReferenceFromString($builder, $referenceString);
 61		}
 62		
 63		// extract links
 64		$links = array();
 65		if ($builder->hasOption('link-to')) {
 66			$links = (array) $builder->getOption('link-to');
 67			$builder->removeOption('link-to');
 68		}
 69		
 70		// adds links
 71		foreach ($links as $linkString) {
 72			self::addLinkFromString($builder, $linkString);
 73		}
 74		
 75		// extract behaviours
 76		$behaviours = array();
 77		if ($builder->hasOption('act-as')) {
 78			$behaviours = (array) $builder->getOption('act-as');
 79			$builder->removeOption('act-as');
 80		}
 81		
 82		// adds behaviours
 83		foreach ($behaviours as $behaviourString) {
 84			foreach (explode(',', $behaviourString) as $behaviour) {
 85				$builder->getBehaviourBroker()->addBehaviour(
 86					Atomik_Model_Behaviour_Factory::factory(trim($behaviour)));
 87			}
 88		}
 89		
 90		return $builder;
 91	}
 92	
 93	/**
 94	 * Returns a builder without the references
 95	 * 
 96	 * @param	string	$className
 97	 * @return 	Atomik_Model_Builder
 98	 */
 99	private static function _getBaseBuilder($className)
100	{
101		if (isset(self::$_cache[$className])) {
102			return self::$_cache[$className];
103		}
104		
105		if (!class_exists($className)) {
106			require_once 'Atomik/Model/Builder/Exception.php';
107			throw new Atomik_Model_Builder_Exception('Class ' . $className . ' not found');
108		}
109		
110		$class = new ReflectionClass($className);
111		$builder = new Atomik_Model_Builder($className, $className);
112		
113		foreach ($class->getProperties(ReflectionProperty::IS_PUBLIC) as $prop) {
114			$propData = self::getMetadataFromDocBlock($prop->getDocComment());
115			
116			// jump to the next property if there is the ignore tag
117			if (isset($propData['ignore'])) {
118				continue;
119			}
120			
121			$type = 'string';
122			if (isset($propData['var'])) {
123				$type = $propData['var'];
124				unset($propData['var']);
125			} else if (!isset($propData['length'])) {
126				$propData['length'] = 255;
127			}
128			
129			$field = Atomik_Model_Field_Factory::factory($type, $prop->getName(), $propData);
130			$builder->addField($field);
131		}
132		
133		$options = self::getMetadataFromDocBlock($class->getDocComment());
134		
135		// sets the adapter
136		if (isset($options['table'])) {
137			$builder->tableName = $options['table'];
138			unset($options['table']);
139		}
140		
141		// use the remaining metadatas as options
142		$builder->setOptions($options);
143		
144		if (($parentClass = $class->getParentClass()) != null) {
145			if ($parentClass->getName() != 'Atomik_Model' && $parentClass->isSubclassOf('Atomik_Model')) {
146				$builder->setParentModel($parentClass->getName());
147			}
148		}
149		
150		self::$_cache[$className] = $builder;
151		return $builder;
152	}
153	
154	/**
155	 * Retreives metadata tags (i.e. the one starting with @) from a doc block
156	 *
157	 * @param 	string 	$doc
158	 * @return 	array
159	 */
160	public static function getMetadataFromDocBlock($doc)
161	{
162		$metadata = array();
163		preg_match_all('/@(.+)$/mU', $doc, $matches);
164		
165		for ($i = 0, $c = count($matches[0]); $i < $c; $i++) {
166			if (($separator = strpos($matches[1][$i], ' ')) !== false) {
167				$key = trim(substr($matches[1][$i], 0, $separator));
168				$value = trim(substr($matches[1][$i], $separator + 1));
169				// boolean
170				if ($value == 'false') {
171					$value = false;
172				} else if ($value == 'true') {
173					$value = true;
174				}
175			} else {
176				$key = trim($matches[1][$i]);
177				$value = true;
178			}
179			
180			if (isset($metadata[$key])) {
181				if (!is_array($metadata[$key])) {
182					$metadata[$key] = array($metadata[$key]);
183				}
184				$metadata[$key][] = $value;
185			} else {
186				$metadata[$key] = $value;
187			}
188		}
189		
190		return $metadata;
191	}
192	
193	/**
194	 * Builds a reference object from a string and adds it to the builder. The string should follow the following pattern:
195	 * (one|many) foreignModel [as property] [using localModel.localField = foreignModel.foreignField] [order by field] [limit offset, length]
196	 *
197	 * @param	Atomik_Model_Builder	$builder
198	 * @param 	string 					$string
199	 * @return 	Atomik_Model_Builder_Reference
200	 */
201	public static function addReferenceFromString(Atomik_Model_Builder $builder, $string)
202	{
203		$regexp = '/(?P<type>one|many|parent)\s+(?P<target>.+)((\sas\s(?P<as>.+))|)((\svia\s(?P<viatype>table|model|)\s(?P<via>.+))|)'
204				. '((\susing\s(?P<using>.+))|)((\sorder by\s(?P<order>.+))|)((\slimit\s(?P<limit>.+))|)$/U';
205				
206		if (!preg_match($regexp, $string, $matches)) {
207			require_once 'Atomik/Model/Builder/Exception.php';
208			throw new Atomik_Model_Builder_Exception('Reference string is malformed: ' . $string);
209		}
210		
211		// type and target
212		$type = $matches['type'];
213		$target = trim($matches['target']);
214		
215		// name
216		$name = $target;
217		if (isset($matches['as']) && !empty($matches['as'])) {
218			$name = trim($matches['as']);
219		}
220		
221		$reference = new Atomik_Model_Builder_Reference($name, $type);
222		
223		// via
224		if (isset($matches['via']) && !empty($matches['via'])) {
225			// @TODO support via in model references
226		}
227		
228		// fields
229		if (isset($matches['using']) && !empty($matches['using'])) {
230			list($sourceField, $targetField) = self::getReferenceFieldsFromString($matches['using'], $builder->name);
231		} else {
232			list($sourceField, $targetField) = self::getReferenceFields($builder, $target, $type);
233		}
234		$reference->sourceField = $sourceField;
235		$reference->targetField = $targetField;
236		
237		// order by
238		if (isset($matches['order']) && !empty($matches['order'])) {
239			$reference->query->orderBy($matches['order']);
240		}
241		
242		// limit
243		if (isset($matches['limit']) && !empty($matches['limit'])) {
244			$reference->query->limit($matches['limit']);
245		}
246		
247		$reference->target = $target;
248		$builder->addReference($reference);
249		return $reference;
250	}
251	
252	/**
253	 * Returns reference fields depending on the type of reference
254	 * 
255	 * @param	Atomik_Model_Builder	$builder
256	 * @param	string					$targetName
257	 * @param	string					$type
258	 * @return 	array					array(sourceField, targetField)
259	 */
260	public static function getReferenceFields(Atomik_Model_Builder $builder, $targetName, $type)
261	{
262		$targetBuilder = self::_getBaseBuilder($targetName);
263		
264		if ($type == Atomik_Model_Builder_Reference::HAS_PARENT) {
265			// targetModel.targetPrimaryKey = sourceModel.targetModel_targetPrimaryKey
266			$targetField = $targetBuilder->getPrimaryKeyField()->name;
267			$sourceField = strtolower($targetName) . '_' . $targetField;
268			
269		} else if ($type == Atomik_Model_Builder_Reference::HAS_ONE) {
270			// targetModel.sourceModel_sourcePrimaryKey = sourceModel.sourcePrimaryKey
271			$sourceField = $builder->getPrimaryKeyField()->name;
272			$targetField = strtolower($builder->name) . '_' . $sourceField;
273			
274		} else {
275			$targetBuilder = Atomik_Model_Builder_Factory::get($targetName);
276			
277			// HAS_MANY
278			// searching through the target model references for one pointing back to this model
279			$parentRefs = $targetBuilder->getReferences();
280			$found = false;
281			foreach ($parentRefs as $parentRef) {
282				if ($parentRef->isHasParent() && $parentRef->isTarget($builder->name)) {
283					$sourceField = $parentRef->targetField;
284					$targetField = $parentRef->sourceField;
285					$found = true;
286					break;
287				}
288			}
289			if (!$found) {
290				require_once 'Atomik/Model/Builder/Exception.php';
291				throw new Atomik_Model_Builder_Exception('No back reference in ' . $targetName . ' for ' . $builder->name);
292			}
293		}
294		
295		return array($sourceField, $targetField);
296	}
297	
298	/**
299	 * Returns reference fields computed from a string which should follow 
300	 * the pattern sourceMode.sourceField = targetModel.targetField (or vice versa)
301	 * 
302	 * @param 	string	$string
303	 * @param 	string	$sourceName
304	 * @return 	array	array(sourceField, targetField)
305	 */
306	public static function getReferenceFieldsFromString($string, $sourceName)
307	{
308		if (!preg_match('/(.+)\.(.+)\s(=)\s(.+)\.(.+)/', $string, $matches)) {
309			require_once 'Atomik/Model/Builder/Exception.php';
310			throw new Atomik_Model_Builder_Exception('Using statement for reference is malformed: ' . $string);
311		}
312		
313		if ($matches[1] == $sourceName) {
314			return array($matches[2], $matches[5]);
315		}
316		return array($matches[5], $matches[2]);
317	}
318	
319	/**
320	 * Builds a link from the string and adds it to the builder
321	 * 
322	 * String template:
323	 * [one] Target [as property] on field [=> alias] [, field [=> alias]]
324	 * 
325	 * Example:
326	 * Tweets as tweets on twitter_username => username
327	 * 
328	 * @param 	Atomik_Model_Builder $builder
329	 * @param 	string				 $string
330	 */
331	public static function addLinkFromString(Atomik_Model_Builder $builder, $string)
332	{
333		$regexp = '/^((?P<type>one)\s+|)(?P<target>.+)((\sas\s(?P<as>.+))|)\s+on\s+(?P<on>.+)$/U';
334				
335		if (!preg_match($regexp, $string, $matches)) {
336			require_once 'Atomik/Model/Builder/Exception.php';
337			throw new Atomik_Model_Builder_Exception('Link string is malformed: ' . $string);
338		}
339		
340		$link = new Atomik_Model_Builder_Link();
341		$link->type = $matches['type'] == 'one' ? 'one' : 'many';
342		$link->target = trim($matches['target']);
343		$link->name = $link->target;
344		if (isset($matches['as']) && !empty($matches['as'])) {
345			$link->name = trim($matches['as']);
346		}
347		
348		$fields = explode(',', $matches['on']);
349		foreach ($fields as $field) {
350			if (!preg_match('/^(?P<name>.+)(\s+=\>\s(?P<alias>.+)|)$/U', trim($field), $fieldMatches)) {
351				require_once 'Atomik/Model/Builder/Exception.php';
352				throw new Atomik_Model_Builder_Exception('Field definition in link string is malformed: ' . $string);
353			}
354			$alias = isset($fieldMatches['alias']) ? $fieldMatches['alias'] : $fieldMatches['name'];
355			$link->fields[$alias] = $fieldMatches['name'];
356		}
357		
358		$builder->addLink($link);
359	}
360}