PageRenderTime 36ms CodeModel.GetById 13ms app.highlight 17ms RepoModel.GetById 1ms app.codeStats 1ms

/lib/m2.php

https://gitlab.com/f3/f3-skeleton
PHP | 541 lines | 290 code | 32 blank | 219 comment | 39 complexity | 5d7b54b208eb2b46d47b288a76974558 MD5 | raw file
  1<?php
  2
  3/**
  4	MongoDB Mapper for the PHP Fat-Free Framework
  5
  6	The contents of this file are subject to the terms of the GNU General
  7	Public License Version 3.0. You may not use this file except in
  8	compliance with the license. Any of the license terms and conditions
  9	can be waived if you get permission from the copyright holder.
 10
 11	Copyright (c) 2009-2011 F3::Factory
 12	Bong Cosca <bong.cosca@yahoo.com>
 13
 14		@package M2
 15		@version 2.0.5
 16**/
 17
 18//! MongoDB Mapper
 19class M2 extends Base {
 20
 21	//@{ Locale-specific error/exception messages
 22	const
 23		TEXT_M2Connect='Undefined database',
 24		TEXT_M2Empty='M2 is empty',
 25		TEXT_M2Collection='Collection %s does not exist';
 26	//@}
 27
 28	//@{
 29	//! M2 properties
 30	private
 31		$db,$collection,$object,$cond,$seq,$ofs;
 32	//@}
 33
 34	/**
 35		M2 factory
 36			@return object
 37			@param $doc array
 38			@public
 39	**/
 40	function factory($doc) {
 41		$self=get_class($this);
 42		$m2=new $self($this->collection,$this->db);
 43		foreach ($doc as $key=>$val)
 44			$m2->object[$key]=$val;
 45		return $m2;
 46	}
 47
 48	/**
 49		Retrieve from cache; or save query results to cache if not
 50		previously executed
 51			@param $query array
 52			@param $ttl int
 53			@private
 54	**/
 55	private function cache(array $query,$ttl) {
 56		$cmd=json_encode($query,TRUE);
 57		$hash='mdb.'.self::hash($cmd);
 58		$cached=Cache::cached($hash);
 59		$db=(string)$this->db;
 60		$stats=&self::ref('STATS');
 61		if ($ttl && $cached && $_SERVER['REQUEST_TIME']-$cached<$ttl) {
 62			// Gather cached queries for profiler
 63			if (!isset($stats[$db]['cache'][$cmd]))
 64				$stats[$db]['cache'][$cmd]=0;
 65			$stats[$db]['cache'][$cmd]++;
 66			// Retrieve from cache
 67			return Cache::get($hash);
 68		}
 69		else {
 70			$result=$this->exec($query);
 71			if ($ttl)
 72				Cache::set($hash,$result,$ttl);
 73			// Gather real queries for profiler
 74			if (!isset($stats[$db]['queries'][$cmd]))
 75				$stats[$db]['queries'][$cmd]=0;
 76			$stats[$db]['queries'][$cmd]++;
 77			return $result;
 78		}
 79	}
 80
 81	/**
 82		Execute MongoDB query
 83			@return mixed
 84			@param $query array
 85			@private
 86	**/
 87	private function exec(array $query) {
 88		$cmd=json_encode($query,TRUE);
 89		$hash='mdb.'.self::hash($cmd);
 90		// Except for save method, collection must exist
 91		$list=$this->db->listCollections();
 92		foreach ($list as &$coll)
 93			$coll=$coll->getName();
 94		if ($query['method']!='save' && !in_array($this->collection,$list)) {
 95			trigger_error(sprintf(self::TEXT_M2Collection,$this->collection));
 96			return;
 97		}
 98		if (isset($query['map'])) {
 99			// Create temporary collection
100			$ref=$this->db->selectCollection($hash);
101			$ref->batchInsert(iterator_to_array($out,FALSE));
102			$map=$query['map'];
103			$func='function() {}';
104			// Map-reduce
105			$tmp=$this->db->command(
106				array(
107					'mapreduce'=>$ref->getName(),
108					'map'=>isset($map['map'])?
109						$map['map']:$func,
110					'reduce'=>isset($map['reduce'])?
111						$map['reduce']:$func,
112					'finalize'=>isset($map['finalize'])?
113						$map['finalize']:$func
114				)
115			);
116			if (!$tmp['ok']) {
117				trigger_error($tmp['errmsg']);
118				return FALSE;
119			}
120			$ref->remove();
121			// Aggregate the result
122			foreach (iterator_to_array($this->db->
123				selectCollection($tmp['result'])->find(),FALSE) as $agg)
124				$ref->insert($agg['_id']);
125			$out=$ref->find();
126			$ref->drop();
127		}
128		elseif (preg_match('/find/',$query['method'])) {
129			// find and findOne methods allow selection of fields
130			$out=call_user_func(
131				array(
132					$this->db->selectCollection($this->collection),
133					$query['method']
134				),
135				isset($query['cond'])?$query['cond']:array(),
136				isset($query['fields'])?$query['fields']:array()
137			);
138			if ($query['method']=='find') {
139				if (isset($query['seq']))
140					// Sort results
141					$out=$out->sort($query['seq']);
142				if (isset($query['ofs']))
143					// Skip to record ofs
144					$out=$out->skip($query['ofs']);
145				if (isset($query['limit']) && $query['limit'])
146					// Limit number of results
147					$out=$out->limit($query['limit']);
148				// Convert cursor to PHP array
149				$out=iterator_to_array($out,FALSE);
150				if ($query['m2'])
151					foreach ($out as &$obj)
152						$obj=$this->factory($obj);
153			}
154		}
155		else
156			$out=preg_match('/count|remove/',$query['method'])?
157				// count() and remove() methods can specify cond
158				call_user_func(
159					array(
160						$this->db->selectCollection($this->collection),
161						$query['method']
162					),
163					isset($query['cond'])?$query['cond']:array()
164				):
165				// All other queries
166				call_user_func(
167					array(
168						$this->db->selectCollection($this->collection),
169						$query['method']
170					),
171					$this->object
172				);
173		return $out;
174	}
175
176	/**
177		Return current object contents as an array
178			@return array
179			@public
180	**/
181	function cast() {
182		return $this->object;
183	}
184
185	/**
186		Similar to M2->find method but provides more fine-grained control
187		over specific fields and map-reduced results
188			@return array
189			@param $fields array
190			@param $cond mixed
191			@param $map mixed
192			@param $seq mixed
193			@param $limit mixed
194			@param $ofs mixed
195			@param $ttl int
196			@param $m2 bool
197			@public
198	**/
199	function lookup(
200		array $fields,
201		$cond=NULL,
202		$map=NULL,
203		$seq=NULL,
204		$limit=0,
205		$ofs=0,
206		$ttl=0,
207		$m2=TRUE) {
208		$query=array(
209			'method'=>'find',
210			'fields'=>$fields,
211			'cond'=>$cond,
212			'map'=>$map,
213			'seq'=>$seq,
214			'limit'=>$limit,
215			'ofs'=>$ofs,
216			'm2'=>$m2
217		);
218		return $ttl?$this->cache($query,$ttl):$this->exec($query);
219	}
220
221	/**
222		Alias of the lookup method
223			@public
224	**/
225	function select() {
226		// PHP doesn't allow direct use as function argument
227		$args=func_get_args();
228		return call_user_func_array(array($this,'lookup'),$args);
229	}
230
231	/**
232		Return an array of collection objects matching cond
233			@return array
234			@param $cond mixed
235			@param $seq mixed
236			@param $limit mixed
237			@param $ofs mixed
238			@param $ttl int
239			@param $m2 bool
240			@public
241	**/
242	function find($cond=NULL,$seq=NULL,$limit=NULL,$ofs=0,$ttl=0,$m2=TRUE) {
243		$query=array(
244			'method'=>'find',
245			'cond'=>$cond,
246			'seq'=>$seq,
247			'limit'=>$limit,
248			'ofs'=>$ofs,
249			'm2'=>$m2
250		);
251		return $ttl?$this->cache($query,$ttl):$this->exec($query);
252	}
253
254	/**
255		Return an array of associative arrays matching cond
256			@return array
257			@param $cond mixed
258			@param $seq mixed
259			@param $limit mixed
260			@param $ofs int
261			@param $ttl int
262			@public
263	**/
264	function afind($cond=NULL,$seq=NULL,$limit=NULL,$ofs=0,$ttl=0) {
265		return $this->find($cond,$seq,$limit,$ofs,$ttl,FALSE);
266	}
267
268	/**
269		Return the first object that matches the specified cond
270			@return array
271			@param $cond mixed
272			@param $seq mixed
273			@param $ofs int
274			@param $ttl int
275			@public
276	**/
277	function findone($cond=NULL,$seq=NULL,$ofs=0,$ttl=0) {
278		list($result)=$this->find($cond,$seq,1,$ofs,$ttl)?:array(NULL);
279		return $result;
280	}
281
282	/**
283		Return the array equivalent of the object matching criteria
284			@return array
285			@param $cond mixed
286			@param $seq mixed
287			@param $ofs int
288			@public
289	**/
290	function afindone($cond=NULL,$seq=NULL,$ofs=0) {
291		list($result)=$this->afind($cond,$seq,1,$ofs)?:array(NULL);
292		return $result;
293	}
294
295	/**
296		Count objects that match condition
297			@return int
298			@param $cond mixed
299			@public
300	**/
301	function found($cond=NULL) {
302		$result=$this->exec(
303			array(
304				'method'=>'count',
305				'cond'=>$cond
306			)
307		);
308		return $result;
309	}
310
311	/**
312		Hydrate M2 with elements from framework array variable, keys of
313		which will be identical to field names in collection object
314			@param $name string
315			@public
316	**/
317	function copyFrom($name) {
318		if (is_array($ref=self::ref($name)))
319			foreach ($ref as $key=>$val)
320				$this->object[$key]=$val;
321	}
322
323	/**
324		Populate framework array variable with M2 properties, keys of
325		which will have names identical to fields in collection object
326			@param $name string
327			@param $fields string
328			@public
329	**/
330	function copyTo($name,$fields=NULL) {
331		if ($this->dry()) {
332			trigger_error(self::TEXT_M2Empty);
333			return FALSE;
334		}
335		if (is_string($fields))
336			$list=preg_split('/[\|;,]/',$fields,0,PREG_SPLIT_NO_EMPTY);
337		foreach (array_keys($this->object) as $field)
338			if (!isset($list) || in_array($field,$list)) {
339				$var=&self::ref($name);
340				$var[$field]=$this->object[$field];
341			}
342	}
343
344	/**
345		Dehydrate M2
346			@public
347	**/
348	function reset() {
349		// Dehydrate
350		$this->object=NULL;
351		$this->cond=NULL;
352		$this->seq=NULL;
353		$this->ofs=NULL;
354	}
355
356	/**
357		Retrieve first collection object that satisfies cond
358			@return mixed
359			@param $cond mixed
360			@param $seq mixed
361			@param $ofs int
362			@public
363	**/
364	function load($cond=NULL,$seq=NULL,$ofs=0) {
365		if ($ofs>-1) {
366			$this->ofs=0;
367			if ($m2=$this->findOne($cond,$seq,$ofs)) {
368				if (method_exists($this,'beforeLoad') &&
369					$this->beforeLoad()===FALSE)
370					return;
371				// Hydrate M2
372				foreach ($m2->object as $key=>$val)
373					$this->object[$key]=$val;
374				list($this->cond,$this->seq,$this->ofs)=
375					array($cond,$seq,$ofs);
376				if (method_exists($this,'afterLoad'))
377					$this->afterLoad();
378				return $this;
379			}
380		}
381		$this->reset();
382		return FALSE;
383	}
384
385	/**
386		Retrieve N-th object relative to current using the same cond
387		that hydrated M2
388			@return mixed
389			@param $count int
390			@public
391	**/
392	function skip($count=1) {
393		if ($this->dry()) {
394			trigger_error(self::TEXT_M2Empty);
395			return FALSE;
396		}
397		return $this->load($this->cond,$this->seq,$this->ofs+$count);
398	}
399
400	/**
401		Return next record
402			@return array
403			@public
404	**/
405	function next() {
406		return $this->skip();
407	}
408
409	/**
410		Return previous record
411			@return array
412			@public
413	**/
414	function prev() {
415		return $this->skip(-1);
416	}
417
418	/**
419		Insert/update collection object
420			@public
421	**/
422	function save() {
423		if ($this->dry() ||
424			method_exists($this,'beforeSave') &&
425			$this->beforeSave()===FALSE)
426			return;
427		// Let the MongoDB driver decide how to persist the
428		// collection object in the database
429		$obj=$this->object;
430		$this->exec(array('method'=>'save'));
431		if (!isset($obj['_id']))
432			// Reload to retrieve MongoID of inserted object
433			$this->object=
434				$this->exec(array('method'=>'findOne','cond'=>$obj));
435		if (method_exists($this,'afterSave'))
436			$this->afterSave();
437	}
438
439	/**
440		Delete collection object and reset M2
441			@public
442	**/
443	function erase() {
444		if (method_exists($this,'beforeErase') &&
445			$this->beforeErase()===FALSE)
446			return;
447		$this->exec(array('method'=>'remove','cond'=>$this->cond));
448		$this->reset();
449		if (method_exists($this,'afterErase'))
450			$this->afterErase();
451	}
452
453	/**
454		Return TRUE if M2 is NULL
455			@return bool
456			@public
457	**/
458	function dry() {
459		return is_null($this->object);
460	}
461
462	/**
463		Synchronize M2 and MongoDB collection
464			@param $coll string
465			@param $db object
466			@public
467	**/
468	function sync($coll,$db=NULL) {
469		if (!in_array('mongo',get_loaded_extensions())) {
470			// MongoDB extension not activated
471			trigger_error(sprintf(self::TEXT_PHPExt,'mongo'));
472			return;
473		}
474		if (!$db) {
475			if (isset(self::$vars['DB']) && is_a(self::$vars['DB'],'MongoDB'))
476				$db=self::$vars['DB'];
477			else {
478				trigger_error(self::TEXT_M2Connect);
479				return;
480			}
481		}
482		if (method_exists($this,'beforeSync') &&
483			$this->beforeSync()===FALSE)
484			return;
485		// Initialize M2
486		list($this->db,$this->collection)=array($db,$coll);
487		if (method_exists($this,'afterSync'))
488			$this->afterSync();
489	}
490
491	/**
492		Return value of M2-mapped field
493			@return bool
494			@param $name string
495			@public
496	**/
497	function &__get($name) {
498		return $this->object[$name];
499	}
500
501	/**
502		Assign value to M2-mapped field
503			@return bool
504			@param $name string
505			@param $val mixed
506			@public
507	**/
508	function __set($name,$val) {
509		$this->object[$name]=$val;
510	}
511
512	/**
513		Clear value of M2-mapped field
514			@return bool
515			@param $name string
516			@public
517	**/
518	function __unset($name) {
519		unset($this->object[$name]);
520	}
521
522	/**
523		Return TRUE if M2-mapped field exists
524			@return bool
525			@param $name string
526			@public
527	**/
528	function __isset($name) {
529		return array_key_exists($name,$this->object);
530	}
531
532	/**
533		Class constructor
534			@public
535	**/
536	function __construct() {
537		// Execute mandatory sync method
538		call_user_func_array(array($this,'sync'),func_get_args());
539	}
540
541}