Arbit - project tracking

PHPillow - PHP CouchDB connector

Browse source code

File: / src/ classes/ document.php

Type
text/plain text/plain
Last Author
hco
Version
183
Line Rev. Author Source
1 1 kore <?php
2 kore /**
3 2 kore * phpillow CouchDB backend
4 1 kore *
5 2 kore * This file is part of phpillow.
6 1 kore *
7 3 kore * phpillow is free software; you can redistribute it and/or modify it under
8 kore * the terms of the GNU Lesser General Public License as published by the Free
9 kore * Software Foundation; version 3 of the License.
10 1 kore *
11 3 kore * phpillow is distributed in the hope that it will be useful, but WITHOUT ANY
12 kore * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
13 kore * FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for
14 kore * more details.
15 1 kore *
16 3 kore * You should have received a copy of the GNU Lesser General Public License
17 kore * along with phpillow; if not, write to the Free Software Foundation, Inc., 51
18 kore * Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
19 1 kore *
20 kore * @package Core
21 4 kore * @version $Revision: 183 $
22 3 kore * @license http://www.gnu.org/licenses/lgpl-3.0.txt LGPL
23 1 kore */
24 kore
25 kore /**
26 kore * Basic abstract document
27 kore *
28 kore * @package Core
29 4 kore * @version $Revision: 183 $
30 3 kore * @license http://www.gnu.org/licenses/lgpl-3.0.txt LGPL
31 1 kore */
32 3 kore abstract class phpillowDocument
33 1 kore {
34 kore /**
35 kore * Object storing all the document properties as public attributes. This
36 kore * way it is easy to serialize using json_encode.
37 151 seldaek *
38 1 kore * @var StdClass
39 kore */
40 kore protected $storage;
41 kore
42 kore /**
43 kore * Properties with they type and value validators
44 kore *
45 kore * array(
46 kore * ...,
47 3 kore * email => new phpillowMailValidator( ... ),
48 1 kore * ...
49 kore * )
50 151 seldaek *
51 1 kore * @var array
52 kore */
53 kore protected $properties = array();
54 kore
55 kore /**
56 kore * List of required properties. For each required property, which is not
57 kore * set, a validation exception will be thrown on save.
58 151 seldaek *
59 1 kore * @var array
60 kore */
61 kore protected $requiredProperties = array();
62 kore
63 kore /**
64 kore * Document type, may be a string matching the regular expression:
65 kore * (^[a-zA-Z0-9_]+$)
66 151 seldaek *
67 1 kore * @var string
68 kore */
69 kore protected static $type = '_default';
70 kore
71 kore /**
72 159 kore * Indicates whether to keep old revisions of this document or not.
73 1 kore *
74 kore * @var bool
75 kore */
76 kore protected $versioned = true;
77 kore
78 kore /**
79 kore * Flag, indicating if current document has already been modified
80 151 seldaek *
81 1 kore * @var bool
82 kore */
83 kore protected $modified = false;
84 kore
85 kore /**
86 kore * Flag, indicating if current document is a new one.
87 151 seldaek *
88 1 kore * @var bool
89 kore */
90 kore protected $newDocument = true;
91 kore
92 kore /**
93 kore * List of special properties, which are available beside the document
94 kore * specific properties.
95 kore *
96 kore * @var array
97 kore */
98 kore protected static $specialProperties = array(
99 kore '_id',
100 kore '_rev',
101 25 kore '_attachments',
102 1 kore 'type',
103 kore 'revisions',
104 kore );
105 kore
106 kore /**
107 159 kore * List of new attachments to the document.
108 151 seldaek *
109 25 kore * @var array
110 kore */
111 kore protected $newAttachments = array();
112 166 hco
113 hco /**
114 hco * The phpillowConnection to be used by this document
115 hco *
116 hco * Set to null if you want to use phpillowConnection::getInstance()
117 hco *
118 hco * @var phpillowConnection
119 hco */
120 hco protected $connection = null;
121 hco
122 hco /**
123 hco * The database to be used by this document
124 hco *
125 hco * Set to null if you want to use phpillowConnection::getDatabase()
126 hco *
127 hco * @var string
128 hco */
129 hco protected $database = null;
130 25 kore
131 kore /**
132 94 kore * Set this before calling static functions.
133 kore *
134 kore * @var string
135 kore */
136 kore public static $docType = null;
137 kore
138 kore /**
139 1 kore * Construct new document
140 151 seldaek *
141 1 kore * Construct new document
142 151 seldaek *
143 1 kore * @return void
144 kore */
145 94 kore public function __construct()
146 1 kore {
147 kore $this->storage = new StdClass();
148 kore $this->storage->revisions = array();
149 kore $this->storage->_id = null;
150 25 kore $this->storage->_attachments = array();
151 1 kore
152 kore // Set all defined properties to null on construct
153 kore foreach ( $this->properties as $property => $v )
154 kore {
155 kore $this->storage->$property = null;
156 kore }
157 kore
158 kore // Also store document type in document
159 94 kore $this->storage->type = $this->getType();
160 1 kore }
161 kore
162 kore /**
163 kore * Get document property
164 151 seldaek *
165 1 kore * Get property from document
166 kore *
167 151 seldaek * @param string $property
168 1 kore * @return mixed
169 kore */
170 kore public function __get( $property )
171 kore {
172 kore // Check if property exists as a custom document property
173 kore if ( isset( $this->properties[$property] ) )
174 kore {
175 kore return $this->storage->$property;
176 kore }
177 kore
178 kore // Check if the requested property is one of the special properties,
179 kore // which are available for all documents
180 kore if ( in_array( $property, self::$specialProperties ) )
181 kore {
182 kore return $this->storage->$property;
183 kore }
184 kore
185 kore // If none of the above checks passed, the request is invalid.
186 3 kore throw new phpillowNoSuchPropertyException( $property );
187 1 kore }
188 kore
189 kore /**
190 kore * Set a property value
191 kore *
192 kore * Set a property value, which will be validated using the assigned
193 kore * validator. Setting a property will mark the document as modified, so
194 kore * that you know when to store the object.
195 151 seldaek *
196 seldaek * @param string $property
197 seldaek * @param mixed $value
198 1 kore * @return void
199 kore */
200 kore public function __set( $property, $value )
201 kore {
202 kore // Check if property exists at all
203 kore if ( !isset( $this->properties[$property] ) )
204 kore {
205 3 kore throw new phpillowNoSuchPropertyException( $property );
206 1 kore }
207 kore
208 kore // Check if the passed value meets the property validation, and perform
209 159 kore // necessary transformation, like typecasts, or similar.
210 1 kore //
211 kore // If the value could not be fixed, this may throw an exception.
212 kore $value = $this->properties[$property]->validate( $value );
213 kore
214 159 kore // Store value in storage object and mark document modified
215 1 kore $this->storage->$property = $value;
216 kore $this->modified = true;
217 kore }
218 kore
219 kore /**
220 68 kore * Check if document property is set
221 151 seldaek *
222 68 kore * Check if document property is set
223 kore *
224 151 seldaek * @param string $property
225 68 kore * @return boolean
226 kore */
227 kore public function __isset( $property )
228 kore {
229 kore // Check if property exists as a custom document property
230 kore if ( array_key_exists( $property, $this->properties ) ||
231 kore in_array( $property, self::$specialProperties ) )
232 kore {
233 kore return true;
234 kore }
235 kore
236 kore // If none of the above checks passed, the request is invalid.
237 kore return false;
238 kore }
239 kore
240 kore /**
241 1 kore * Set values from a response object
242 kore *
243 kore * Set values of the document from the response object, if they are
244 kore * available in there.
245 151 seldaek *
246 seldaek * @param phpillowResponse $response
247 1 kore * @return void
248 kore */
249 3 kore protected function fromResponse( phpillowResponse $response )
250 1 kore {
251 kore // Set all document property values from response, if available in the
252 kore // response.
253 kore //
254 159 kore // Also fill a revision object with the set attributes, so that the
255 1 kore // current revision is also available in history, and it is stored,
256 kore // when the object is modified and stored again.
257 kore $revision = new StdClass();
258 kore $revision->_date = time();
259 151 seldaek foreach ( $this->properties as $property => $v )
260 1 kore {
261 kore if ( isset( $response->$property ) )
262 kore {
263 kore $this->storage->$property = $response->$property;
264 kore $revision->$property = $response->$property;
265 kore }
266 kore }
267 kore
268 kore // Set special properties from response object
269 kore $this->storage->_rev = $response->_rev;
270 25 kore $this->storage->_id = $response->_id;
271 1 kore
272 159 kore // Set attachments array, if the response object contains attachments.
273 25 kore if ( isset( $response->_attachments ) )
274 kore {
275 kore $this->storage->_attachments = $response->_attachments;
276 kore }
277 kore
278 1 kore // Check if the source document already contains a revision history and
279 kore // store it in this case in the document object, if the object should
280 kore // be versioned at all.
281 kore if ( $this->versioned )
282 kore {
283 kore if ( isset( $response->revisions ) )
284 kore {
285 kore $this->storage->revisions = $response->revisions;
286 kore }
287 kore
288 kore // Add current revision to revision history
289 30 kore $this->storage->revisions[] = (array) $revision;
290 1 kore }
291 kore
292 kore // Document freshly loaded, so it is not modified, and not a new
293 kore // document...
294 kore $this->modified = false;
295 kore $this->newDocument = false;
296 kore }
297 kore
298 kore /**
299 kore * Get document ID from object ID
300 kore *
301 kore * Composes the document ID out of the document type and the generated ID
302 kore * for the current document.
303 40 kore *
304 159 kore * If null is provided as an ID, we keep this value and do not construct
305 40 kore * something else, to let the server autogenerate some ID.
306 151 seldaek *
307 seldaek * @param string $type
308 seldaek * @param mixed $id
309 40 kore * @return mixed
310 1 kore */
311 94 kore protected function getDocumentId( $type, $id )
312 1 kore {
313 40 kore return ( $id === null ? null : $type . '-' . $id );
314 1 kore }
315 kore
316 kore /**
317 kore * Get document by ID
318 kore *
319 151 seldaek * Get document by ID and return a document object instance for the fetch
320 1 kore * document.
321 151 seldaek *
322 seldaek * @param string $id
323 3 kore * @return phpillowDocument
324 1 kore */
325 94 kore public function fetchById( $id )
326 1 kore {
327 kore // If a fetch is called with an empty ID, we throw an exception, as we
328 kore // would get database statistics otherwise, and the following error may
329 kore // be hard to debug.
330 kore if ( empty( $id ) )
331 kore {
332 151 seldaek throw new phpillowResponseNotFoundErrorException( array(
333 30 kore 'error' => 'not_found',
334 kore 'reason' => 'No document ID specified.',
335 kore ) );
336 1 kore }
337 kore
338 kore // Fetch object from database
339 166 hco $db = $this->getConnection();
340 151 seldaek $response = $db->get(
341 166 hco $this->getDatabase() . urlencode( $id )
342 1 kore );
343 kore
344 160 kore // Check if type of response matches type of class
345 kore $this->checkTypeOfResponse( $response );
346 kore
347 94 kore // Create document contents from fetched object
348 kore $this->fromResponse( $response );
349 1 kore
350 94 kore return $this;
351 1 kore }
352 kore
353 kore /**
354 160 kore * Verifies that the fetched document is of the given type
355 kore *
356 kore * @param phpillowResponse $response
357 kore * @return void
358 kore */
359 kore public function checkTypeOfResponse( phpillowResponse $response )
360 kore {
361 kore if ( $response->type != $this->getType() )
362 kore {
363 kore throw new phpillowResponseNotFoundErrorException(
364 kore array(
365 kore 'error' => 'mismatch',
366 kore 'reason' => 'Type does not match: ' . $response->type . ' != ' . $this->getType(),
367 kore )
368 kore );
369 kore }
370 kore }
371 kore
372 kore /**
373 94 kore * Create a new instance of the document class
374 1 kore *
375 94 kore * Create a new instance of the statically called document class.
376 kore * Implementing this method should only be required when using PHP 5.2 and
377 kore * lower, otherwise the class can be determined using LSB.
378 kore *
379 kore * Do not pass a parameter to this method, this is only used to maintain
380 kore * the called class information for PHP 5.2 and lower.
381 kore *
382 kore * @param mixed $docType
383 159 kore * @return phpillowDocument
384 1 kore */
385 94 kore public static function createNew( $docType = null )
386 1 kore {
387 94 kore if ( ( $docType === null ) &&
388 kore function_exists( 'get_called_class' ) )
389 kore {
390 kore $docType = get_called_class();
391 kore }
392 kore elseif ( $docType === null )
393 kore {
394 kore throw new phpillowRuntimeException( 'Invalid docType provided to createNew.' );
395 kore }
396 kore
397 1 kore return new $docType();
398 kore }
399 kore
400 kore /**
401 94 kore * Return document type name
402 kore *
403 kore * This method is required to be implemented to return the document type
404 109 rmehner * for PHP versions lower than 5.3. When only using PHP 5.3 and higher you
405 94 kore * might just implement a method which does "return static:$type" in a base
406 kore * class.
407 151 seldaek *
408 94 kore * @return string
409 kore */
410 kore abstract protected function getType();
411 kore
412 kore /**
413 1 kore * Get ID from document
414 kore *
415 kore * The ID normally should be calculated on some meaningful / unique
416 159 kore * property for the current type of documents. The returned string should
417 1 kore * not be too long and should not contain multibyte characters.
418 41 kore *
419 kore * You can return null instead of an ID string, to trigger the ID
420 kore * autogeneration.
421 151 seldaek *
422 41 kore * @return mixed
423 1 kore */
424 kore abstract protected function generateId();
425 kore
426 kore /**
427 kore * Check if all requirements are met
428 kore *
429 kore * Checks if all required properties has been set. Returns an array with
430 159 kore * the properties, which are required but not set, or true if all
431 1 kore * requirements are fulfilled.
432 151 seldaek *
433 1 kore * @return mixed
434 kore */
435 kore public function checkRequirements()
436 kore {
437 kore // Iterate over properties and check if they are set and not null
438 kore $errors = array();
439 kore foreach ( $this->requiredProperties as $property )
440 kore {
441 kore if ( !isset( $this->storage->$property ) ||
442 kore ( $this->storage->$property === null ) )
443 kore {
444 kore $errors[] = $property;
445 kore }
446 kore }
447 kore
448 kore // If error array is still empty all requirements are met
449 kore if ( $errors === array() )
450 kore {
451 kore return true;
452 kore }
453 kore
454 kore // Otherwise return the array with errors
455 kore return $errors;
456 kore }
457 kore
458 kore /**
459 kore * Save the document
460 kore *
461 159 kore * If thew document has not been modified the method will immediately exit
462 1 kore * and return false. If the document has been been modified, the modified
463 kore * document will be stored in the database, keeping all the old revision
464 kore * intact and return true on success.
465 40 kore *
466 kore * On successful creation the (generated) ID will be returned.
467 151 seldaek *
468 40 kore * @return string
469 1 kore */
470 kore public function save()
471 kore {
472 94 kore // Get document type
473 kore $type = $this->getType();
474 kore
475 1 kore // Ensure all requirements are checked, otherwise bail out with a
476 kore // runtime exception.
477 kore if ( $this->checkRequirements() !== true )
478 kore {
479 2 kore throw new phpillowRuntimeException(
480 1 kore 'Requirements not checked before storing the document.'
481 kore );
482 kore }
483 kore
484 kore // Check if we need to store the stuff at all
485 42 kore if ( ( $this->modified === false ) &&
486 kore ( $this->newDocument !== true ) )
487 1 kore {
488 kore return false;
489 kore }
490 kore
491 kore // Generate a new ID, if this is a new document, otherwise reuse the
492 kore // existing document ID.
493 42 kore if ( $this->newDocument === true )
494 1 kore {
495 94 kore $this->storage->_id = $this->getDocumentId( $type, $this->generateId() );
496 1 kore }
497 kore
498 159 kore // Do not send an attachment array, if there aren't any attachments
499 94 kore if ( !isset( $this->storage->_attachments ) ||
500 kore !count( $this->storage->_attachments ) )
501 25 kore {
502 kore unset( $this->storage->_attachments );
503 kore }
504 kore
505 40 kore // If the document ID is null, the server should autogenerate some ID,
506 kore // but for this we need to use a different request method.
507 166 hco $db = $this->getConnection();
508 40 kore if ( $this->storage->_id === null )
509 kore {
510 kore // Store document in database
511 kore unset( $this->storage->_id );
512 kore $response = $db->post(
513 166 hco $this->getDatabase(),
514 40 kore json_encode( $this->storage )
515 kore );
516 kore }
517 kore else
518 kore {
519 kore // Store document in database
520 kore $response = $db->put(
521 166 hco $this->getDatabase() . urlencode( $this->_id ),
522 40 kore json_encode( $this->storage )
523 kore );
524 kore }
525 1 kore
526 183 hco $this->storage->_rev = $response->rev;
527 hco
528 168 jakob // Restore the __attachments array if it has been removed before
529 jakob if ( !isset( $this->storage->_attachments ) )
530 jakob {
531 jakob $this->storage->_attachments = array();
532 jakob }
533 183 hco
534 hco // This document is no longer new
535 hco $this->newDocument = false;
536 hco
537 40 kore return $this->storage->_id = $response->id;
538 1 kore }
539 kore
540 kore /**
541 157 kore * Deletes the current document
542 kore *
543 kore * Tries to delete the current document from the database. Might throw a
544 kore * conflict exception in case the document has been modified since the last
545 kore * fetch.
546 kore *
547 kore * @return void
548 kore */
549 kore public function delete()
550 kore {
551 166 hco $db = $this->getConnection();
552 157 kore return $db->delete(
553 166 hco $this->getDatabase() . urlencode( $this->_id ) . '?rev=' . $this->_rev
554 157 kore );
555 kore }
556 kore
557 kore /**
558 159 kore * Get ID string from arbitrary string
559 1 kore *
560 2 kore * To calculate an ID string from an phpillowrary string, first iconvs
561 159 kore * transliteration abilities are used, and after that all, but common ID
562 1 kore * characters, are replaced by the given replace string, which defaults to
563 kore * _.
564 151 seldaek *
565 seldaek * @param string $string
566 seldaek * @param string $replace
567 1 kore * @return string
568 kore */
569 kore protected function stringToId( $string, $replace = '_' )
570 kore {
571 kore // First translit string to ASCII, as this characters are most probably
572 kore // supported everywhere
573 kore $string = iconv( 'UTF-8', 'ASCII//TRANSLIT', $string );
574 kore
575 159 kore // And then still replace any obscure characters by _ to ensure nothing
576 1 kore // "bad" happens with this string.
577 kore $string = preg_replace( '([^A-Za-z0-9.-]+)', $replace, $string );
578 kore
579 kore // Additionally we convert the string to lowercase, so that we get case
580 kore // insensitive fetching
581 kore return strtolower( $string );
582 kore }
583 25 kore
584 kore /**
585 kore * Attach file to document
586 27 kore *
587 kore * The file passed to the method will be attached to the document and
588 kore * stored in the database. By default the filename of the provided file
589 kore * will be ued as a name, but you may optionally specify a name as the
590 kore * second parameter of the method.
591 28 kore *
592 kore * You may optionally specify a custom mime type as third parameter. If set
593 kore * it will be used, but not verified, that it matches the actual file
594 kore * contents. If left empty the mime type defaults to
595 kore * 'application/octet-stream'.
596 151 seldaek *
597 seldaek * @param string $fileName
598 27 kore * @param string $name
599 28 kore * @param string $mimeType
600 25 kore * @return void
601 kore */
602 28 kore public function attachFile( $fileName, $name = false, $mimeType = false )
603 25 kore {
604 27 kore $name = ( $name === false ? basename( $fileName ) : $name );
605 175 jakob
606 jakob $this->attachMemoryFile(
607 jakob file_get_contents( $fileName ),
608 jakob $name,
609 jakob $mimeType
610 jakob );
611 jakob }
612 jakob
613 jakob /**
614 jakob * Attach file from memory to document
615 jakob *
616 jakob * The data passed to the method will be attached to the document and
617 jakob * stored in the database.
618 jakob *
619 jakob * You need to specify a name to be used for storing the attachment data.
620 jakob *
621 jakob * You may optionally specify a custom mime type as third parameter. If set
622 jakob * it will be used, but not verified, that it matches the actual file
623 jakob * contents. If left empty the mime type defaults to
624 jakob * 'application/octet-stream'.
625 jakob *
626 jakob * @param string $data
627 jakob * @param string $name
628 jakob * @param string $mimeType
629 jakob * @return void
630 jakob */
631 jakob public function attachMemoryFile( $data, $name, $mimeType = false )
632 jakob {
633 27 kore $this->storage->_attachments[$name] = array(
634 28 kore 'type' => 'base64',
635 175 jakob 'data' => base64_encode( $data ),
636 28 kore 'content_type' => $mimeType === false ? 'application/octet-stream' : $mimeType,
637 25 kore );
638 27 kore $this->modified = true;
639 25 kore }
640 kore
641 kore /**
642 kore * Get file contents
643 kore *
644 33 kore * Get the contents of an attached file as a phpillowDataResponse.
645 151 seldaek *
646 seldaek * @param string $fileName
647 178 jakob * @return phpillowLazyFile
648 25 kore */
649 kore public function getFile( $fileName )
650 kore {
651 kore if ( !isset( $this->storage->_attachments[$fileName] ) )
652 kore {
653 kore throw new phpillowNoSuchPropertyException( $fileName );
654 kore }
655 kore
656 178 jakob $attachment = $this->storage->_attachments[$fileName];
657 jakob
658 jakob return new phpillowLazyFile(
659 jakob $this->getConnection(),
660 166 hco $this->getDatabase() . urlencode( $this->_id ) . '/' . $fileName,
661 178 jakob $attachment['content_type'],
662 jakob $attachment['length']
663 25 kore );
664 178 jakob }
665 25 kore
666 166 hco /**
667 hco * Return used connection
668 hco *
669 hco * This should always used within a document instead of
670 hco * phpillowConnection::getInstance()
671 hco *
672 hco * @return phpillowConnection
673 hco */
674 hco public function getConnection()
675 hco {
676 hco if ( $this->connection === null ) {
677 hco return phpillowConnection::getInstance();
678 hco }
679 hco
680 hco return $this->connection;
681 hco }
682 hco
683 hco /**
684 hco * Reconfigure the connection to be used by this document
685 hco *
686 hco * @param phpillowConnection $connection
687 hco * @return void
688 hco */
689 hco public function setConnection( phpillowConnection $connection )
690 hco {
691 hco $this->connection = $connection;
692 hco }
693 hco
694 hco /**
695 hco * Return used database
696 hco *
697 hco * This should always used within a document instead of
698 hco * phpillowConnection::getDatabase()
699 hco *
700 hco * @return phpillowConnection
701 hco */
702 hco public function getDatabase()
703 hco {
704 hco if ( $this->database === null ) {
705 hco return phpillowConnection::getDatabase();
706 hco }
707 hco
708 hco return $this->database;
709 hco }
710 hco
711 hco /**
712 hco * Reconfigure the database to be used by this document
713 hco *
714 hco * @param string $database
715 hco * @return void
716 hco */
717 hco public function setDatabase( $database )
718 hco {
719 167 jakob $this->database = '/' . $database . '/';
720 166 hco }
721 1 kore }
722 kore