1: <?php
2:
3: namespace RedBeanPHP\Repository;
4:
5: use RedBeanPHP\OODBBean as OODBBean;
6: use RedBeanPHP\QueryWriter as QueryWriter;
7: use RedBeanPHP\RedException as RedException;
8: use RedBeanPHP\BeanHelper as BeanHelper;
9: use RedBeanPHP\RedException\SQL as SQLException;
10: use RedBeanPHP\Repository as Repository;
11:
12: /**
13: * Fluid Repository.
14: * OODB manages two repositories, a fluid one that
15: * adjust the database schema on-the-fly to accomodate for
16: * new bean types (tables) and new properties (columns) and
17: * a frozen one for use in a production environment. OODB
18: * allows you to swap the repository instances using the freeze()
19: * method.
20: *
21: * @file RedBeanPHP/Repository/Fluid.php
22: * @author Gabor de Mooij and the RedBeanPHP community
23: * @license BSD/GPLv2
24: *
25: * @copyright
26: * copyright (c) G.J.G.T. (Gabor) de Mooij and the RedBeanPHP Community
27: * This source file is subject to the BSD/GPLv2 License that is bundled
28: * with this source code in the file license.txt.
29: */
30: class Fluid extends Repository
31: {
32: /**
33: * Figures out the desired type given the cast string ID.
34: *
35: * @param string $cast cast identifier
36: *
37: * @return integer
38: */
39: private function getTypeFromCast( $cast )
40: {
41: if ( $cast == 'string' ) {
42: $typeno = $this->writer->scanType( 'STRING' );
43: } elseif ( $cast == 'id' ) {
44: $typeno = $this->writer->getTypeForID();
45: } elseif ( isset( $this->writer->sqltype_typeno[$cast] ) ) {
46: $typeno = $this->writer->sqltype_typeno[$cast];
47: } else {
48: throw new RedException( 'Invalid Cast' );
49: }
50:
51: return $typeno;
52: }
53:
54: /**
55: * Orders the Query Writer to create a table if it does not exist already and
56: * adds a note in the build report about the creation.
57: *
58: * @param OODBBean $bean bean to update report of
59: * @param string $table table to check and create if not exists
60: *
61: * @return void
62: */
63: private function createTableIfNotExists( OODBBean $bean, $table )
64: {
65: //Does table exist? If not, create
66: if ( !$this->tableExists( $this->writer->esc( $table, TRUE ) ) ) {
67: $this->writer->createTable( $table );
68: $bean->setMeta( 'buildreport.flags.created', TRUE );
69: }
70: }
71:
72: /**
73: * Modifies the table to fit the bean data.
74: * Given a property and a value and the bean, this method will
75: * adjust the table structure to fit the requirements of the property and value.
76: * This may include adding a new column or widening an existing column to hold a larger
77: * or different kind of value. This method employs the writer to adjust the table
78: * structure in the database. Schema updates are recorded in meta properties of the bean.
79: *
80: * This method will also apply indexes, unique constraints and foreign keys.
81: *
82: * @param OODBBean $bean bean to get cast data from and store meta in
83: * @param string $property property to store
84: * @param mixed $value value to store
85: *
86: * @return void
87: */
88: private function modifySchema( OODBBean $bean, $property, $value )
89: {
90: $doFKStuff = FALSE;
91: $table = $bean->getMeta( 'type' );
92: $columns = $this->writer->getColumns( $table );
93: $columnNoQ = $this->writer->esc( $property, TRUE );
94: if ( !$this->oodb->isChilled( $bean->getMeta( 'type' ) ) ) {
95: if ( $bean->getMeta( "cast.$property", -1 ) !== -1 ) { //check for explicitly specified types
96: $cast = $bean->getMeta( "cast.$property" );
97: $typeno = $this->getTypeFromCast( $cast );
98: } else {
99: $cast = FALSE;
100: $typeno = $this->writer->scanType( $value, TRUE );
101: }
102: if ( isset( $columns[$this->writer->esc( $property, TRUE )] ) ) { //Is this property represented in the table ?
103: if ( !$cast ) { //rescan without taking into account special types >80
104: $typeno = $this->writer->scanType( $value, FALSE );
105: }
106: $sqlt = $this->writer->code( $columns[$this->writer->esc( $property, TRUE )] );
107: if ( $typeno > $sqlt ) { //no, we have to widen the database column type
108: $this->writer->widenColumn( $table, $property, $typeno );
109: $bean->setMeta( 'buildreport.flags.widen', TRUE );
110: $doFKStuff = TRUE;
111: }
112: } else {
113: $this->writer->addColumn( $table, $property, $typeno );
114: $bean->setMeta( 'buildreport.flags.addcolumn', TRUE );
115: $doFKStuff = TRUE;
116: }
117: if ($doFKStuff) {
118: if (strrpos($columnNoQ, '_id')===(strlen($columnNoQ)-3)) {
119: $destinationColumnNoQ = substr($columnNoQ, 0, strlen($columnNoQ)-3);
120: $indexName = "index_foreignkey_{$table}_{$destinationColumnNoQ}";
121: $this->writer->addIndex($table, $indexName, $columnNoQ);
122: $typeof = $bean->getMeta("sys.typeof.{$destinationColumnNoQ}", $destinationColumnNoQ);
123: $isLink = $bean->getMeta( 'sys.buildcommand.unique', FALSE );
124: //Make FK CASCADING if part of exclusive list (dependson=typeof) or if link bean
125: $isDep = ( $bean->moveMeta( 'sys.buildcommand.fkdependson' ) === $typeof || is_array( $isLink ) );
126: $result = $this->writer->addFK( $table, $typeof, $columnNoQ, 'id', $isDep );
127: //If this is a link bean and all unique columns have been added already, then apply unique constraint
128: if ( is_array( $isLink ) && !count( array_diff( $isLink, array_keys( $this->writer->getColumns( $table ) ) ) ) ) {
129: $this->writer->addUniqueConstraint( $table, $bean->moveMeta('sys.buildcommand.unique') );
130: $bean->setMeta("sys.typeof.{$destinationColumnNoQ}", NULL);
131: }
132: }
133: }
134: }
135: }
136:
137: /**
138: * Part of the store() functionality.
139: * Handles all new additions after the bean has been saved.
140: * Stores addition bean in own-list, extracts the id and
141: * adds a foreign key. Also adds a constraint in case the type is
142: * in the dependent list.
143: *
144: * Note that this method raises a custom exception if the bean
145: * is not an instance of OODBBean. Therefore it does not use
146: * a type hint. This allows the user to take action in case
147: * invalid objects are passed in the list.
148: *
149: * @param OODBBean $bean bean to process
150: * @param array $ownAdditions list of addition beans in own-list
151: *
152: * @return void
153: */
154: protected function processAdditions( $bean, $ownAdditions )
155: {
156: $beanType = $bean->getMeta( 'type' );
157:
158: foreach ( $ownAdditions as $addition ) {
159: if ( $addition instanceof OODBBean ) {
160:
161: $myFieldLink = $beanType . '_id';
162: $alias = $bean->getMeta( 'sys.alias.' . $addition->getMeta( 'type' ) );
163: if ( $alias ) $myFieldLink = $alias . '_id';
164:
165: $addition->$myFieldLink = $bean->id;
166: $addition->setMeta( 'cast.' . $myFieldLink, 'id' );
167:
168: if ($alias) {
169: $addition->setMeta( "sys.typeof.{$alias}", $beanType );
170: } else {
171: $addition->setMeta( "sys.typeof.{$beanType}", $beanType );
172: }
173:
174: $this->store( $addition );
175: } else {
176: throw new RedException( 'Array may only contain OODBBeans' );
177: }
178: }
179: }
180:
181: /**
182: * Stores a cleaned bean; i.e. only scalar values. This is the core of the store()
183: * method. When all lists and embedded beans (parent objects) have been processed and
184: * removed from the original bean the bean is passed to this method to be stored
185: * in the database.
186: *
187: * @param OODBBean $bean the clean bean
188: *
189: * @return void
190: */
191: protected function storeBean( OODBBean $bean )
192: {
193: if ( $bean->getMeta( 'changed' ) ) {
194: $this->check( $bean );
195: $table = $bean->getMeta( 'type' );
196: $this->createTableIfNotExists( $bean, $table );
197:
198: $updateValues = array();
199: foreach ( $bean as $property => $value ) {
200: if ( $property !== 'id' ) {
201: $this->modifySchema( $bean, $property, $value );
202: }
203: if ( $property !== 'id' ) {
204: $updateValues[] = array( 'property' => $property, 'value' => $value );
205: }
206: }
207:
208: $bean->id = $this->writer->updateRecord( $table, $updateValues, $bean->id );
209: $bean->setMeta( 'changed', FALSE );
210: }
211: $bean->setMeta( 'tainted', FALSE );
212: }
213:
214: /**
215: * Handles exceptions. Suppresses exceptions caused by missing structures.
216: *
217: * @param Exception $exception exception
218: *
219: * @return void
220: */
221: protected function handleException( \Exception $exception )
222: {
223: if ( !$this->writer->sqlStateIn( $exception->getSQLState(),
224: array(
225: QueryWriter::C_SQLSTATE_NO_SUCH_TABLE,
226: QueryWriter::C_SQLSTATE_NO_SUCH_COLUMN ) )
227: ) {
228: throw $exception;
229: }
230: }
231:
232: /**
233: * Dispenses a new bean (a OODBBean Bean Object)
234: * of the specified type. Always
235: * use this function to get an empty bean object. Never
236: * instantiate a OODBBean yourself because it needs
237: * to be configured before you can use it with RedBean. This
238: * function applies the appropriate initialization /
239: * configuration for you.
240: *
241: * @param string $type type of bean you want to dispense
242: * @param string $number number of beans you would like to get
243: * @param boolean $alwaysReturnArray if TRUE always returns the result as an array
244: *
245: * @return OODBBean
246: */
247: public function dispense( $type, $number = 1, $alwaysReturnArray = FALSE )
248: {
249: $OODBBEAN = defined( 'REDBEAN_OODBBEAN_CLASS' ) ? REDBEAN_OODBBEAN_CLASS : '\RedBeanPHP\OODBBean';
250: $beans = array();
251: for ( $i = 0; $i < $number; $i++ ) {
252: $bean = new $OODBBEAN;
253: $bean->initializeForDispense( $type, $this->oodb->getBeanHelper() );
254: $this->check( $bean );
255: $this->oodb->signal( 'dispense', $bean );
256: $beans[] = $bean;
257: }
258:
259: return ( count( $beans ) === 1 && !$alwaysReturnArray ) ? array_pop( $beans ) : $beans;
260: }
261:
262: /**
263: * Loads a bean from the object database.
264: * It searches for a OODBBean Bean Object in the
265: * database. It does not matter how this bean has been stored.
266: * RedBean uses the primary key ID $id and the string $type
267: * to find the bean. The $type specifies what kind of bean you
268: * are looking for; this is the same type as used with the
269: * dispense() function. If RedBean finds the bean it will return
270: * the OODB Bean object; if it cannot find the bean
271: * RedBean will return a new bean of type $type and with
272: * primary key ID 0. In the latter case it acts basically the
273: * same as dispense().
274: *
275: * Important note:
276: * If the bean cannot be found in the database a new bean of
277: * the specified type will be generated and returned.
278: *
279: * @param string $type type of bean you want to load
280: * @param integer $id ID of the bean you want to load
281: *
282: * @return OODBBean
283: */
284: public function load( $type, $id )
285: {
286: $bean = $this->dispense( $type );
287: if ( isset( $this->stash[$this->nesting][$id] ) ) {
288: $row = $this->stash[$this->nesting][$id];
289: } else {
290: try {
291: $rows = $this->writer->queryRecord( $type, array( 'id' => array( $id ) ) );
292: } catch ( SQLException $exception ) {
293: if ( $this->writer->sqlStateIn( $exception->getSQLState(),
294: array(
295: QueryWriter::C_SQLSTATE_NO_SUCH_COLUMN,
296: QueryWriter::C_SQLSTATE_NO_SUCH_TABLE )
297: )
298: ) {
299: $rows = 0;
300: }
301: }
302: if ( empty( $rows ) ) {
303: return $bean;
304: }
305: $row = array_pop( $rows );
306: }
307: $bean->importRow( $row );
308: $this->nesting++;
309: $this->oodb->signal( 'open', $bean );
310: $this->nesting--;
311:
312: return $bean->setMeta( 'tainted', FALSE );
313: }
314: }
315: