1: <?php
2:
3: namespace RedBeanPHP;
4:
5: use RedBeanPHP\Adapter\DBAdapter as DBAdapter;
6: use RedBeanPHP\QueryWriter as QueryWriter;
7: use RedBeanPHP\RedException as RedException;
8: use RedBeanPHP\RedException\SQL as SQLException;
9:
10: /**
11: * Association Manager.
12: * Manages simple bean associations.
13: *
14: * @file RedBeanPHP/AssociationManager.php
15: * @author Gabor de Mooij and the RedBeanPHP Community
16: * @license BSD/GPLv2
17: *
18: * @copyright
19: * (c) G.J.G.T. (Gabor) de Mooij and the RedBeanPHP Community.
20: * This source file is subject to the BSD/GPLv2 License that is bundled
21: * with this source code in the file license.txt.
22: */
23: class AssociationManager extends Observable
24: {
25: /**
26: * @var OODB
27: */
28: protected $oodb;
29:
30: /**
31: * @var DBAdapter
32: */
33: protected $adapter;
34:
35: /**
36: * @var QueryWriter
37: */
38: protected $writer;
39:
40: /**
41: * Handles exceptions. Suppresses exceptions caused by missing structures.
42: *
43: * @param Exception $exception exception to handle
44: *
45: * @return void
46: */
47: private function handleException( \Exception $exception )
48: {
49: if ( $this->oodb->isFrozen() || !$this->writer->sqlStateIn( $exception->getSQLState(),
50: array(
51: QueryWriter::C_SQLSTATE_NO_SUCH_TABLE,
52: QueryWriter::C_SQLSTATE_NO_SUCH_COLUMN )
53: )
54: ) {
55: throw $exception;
56: }
57: }
58:
59: /**
60: * Internal method.
61: * Returns the many-to-many related rows of table $type for bean $bean using additional SQL in $sql and
62: * $bindings bindings. If $getLinks is TRUE, link rows are returned instead.
63: *
64: * @param OODBBean $bean reference bean instance
65: * @param string $type target bean type
66: * @param string $sql additional SQL snippet
67: * @param array $bindings bindings for query
68: *
69: * @return array
70: */
71: private function relatedRows( $bean, $type, $sql = '', $bindings = array() )
72: {
73: $ids = array( $bean->id );
74: $sourceType = $bean->getMeta( 'type' );
75: try {
76: return $this->writer->queryRecordRelated( $sourceType, $type, $ids, $sql, $bindings );
77: } catch ( SQLException $exception ) {
78: $this->handleException( $exception );
79: return array();
80: }
81: }
82:
83: /**
84: * Associates a pair of beans. This method associates two beans, no matter
85: * what types. Accepts a base bean that contains data for the linking record.
86: * This method is used by associate. This method also accepts a base bean to be used
87: * as the template for the link record in the database.
88: *
89: * @param OODBBean $bean1 first bean
90: * @param OODBBean $bean2 second bean
91: * @param OODBBean $bean base bean (association record)
92: *
93: * @return mixed
94: */
95: protected function associateBeans( OODBBean $bean1, OODBBean $bean2, OODBBean $bean )
96: {
97: $type = $bean->getMeta( 'type' );
98: $property1 = $bean1->getMeta( 'type' ) . '_id';
99: $property2 = $bean2->getMeta( 'type' ) . '_id';
100:
101: if ( $property1 == $property2 ) {
102: $property2 = $bean2->getMeta( 'type' ) . '2_id';
103: }
104:
105: $this->oodb->store( $bean1 );
106: $this->oodb->store( $bean2 );
107:
108: $bean->setMeta( "cast.$property1", "id" );
109: $bean->setMeta( "cast.$property2", "id" );
110: $bean->setMeta( 'sys.buildcommand.unique', array( $property1, $property2 ) );
111:
112: $bean->$property1 = $bean1->id;
113: $bean->$property2 = $bean2->id;
114:
115: $results = array();
116:
117: try {
118: $id = $this->oodb->store( $bean );
119: $results[] = $id;
120: } catch ( SQLException $exception ) {
121: if ( !$this->writer->sqlStateIn( $exception->getSQLState(),
122: array( QueryWriter::C_SQLSTATE_INTEGRITY_CONSTRAINT_VIOLATION ) )
123: ) {
124: throw $exception;
125: }
126: }
127:
128: return $results;
129: }
130:
131: /**
132: * Constructor
133: *
134: * @param ToolBox $tools toolbox
135: */
136: public function __construct( ToolBox $tools )
137: {
138: $this->oodb = $tools->getRedBean();
139: $this->adapter = $tools->getDatabaseAdapter();
140: $this->writer = $tools->getWriter();
141: $this->toolbox = $tools;
142: }
143:
144: /**
145: * Creates a table name based on a types array.
146: * Manages the get the correct name for the linking table for the
147: * types provided.
148: *
149: * @todo find a nice way to decouple this class from QueryWriter?
150: *
151: * @param array $types 2 types as strings
152: *
153: * @return string
154: */
155: public function getTable( $types )
156: {
157: return $this->writer->getAssocTable( $types );
158: }
159:
160: /**
161: * Associates two beans in a many-to-many relation.
162: * This method will associate two beans and store the connection between the
163: * two in a link table. Instead of two single beans this method also accepts
164: * two sets of beans. Returns the ID or the IDs of the linking beans.
165: *
166: * @param OODBBean|array $beans1 one or more beans to form the association
167: * @param OODBBean|array $beans2 one or more beans to form the association
168: *
169: * @return array
170: */
171: public function associate( $beans1, $beans2 )
172: {
173: if ( !is_array( $beans1 ) ) {
174: $beans1 = array( $beans1 );
175: }
176:
177: if ( !is_array( $beans2 ) ) {
178: $beans2 = array( $beans2 );
179: }
180:
181: $results = array();
182: foreach ( $beans1 as $bean1 ) {
183: foreach ( $beans2 as $bean2 ) {
184: $table = $this->getTable( array( $bean1->getMeta( 'type' ), $bean2->getMeta( 'type' ) ) );
185: $bean = $this->oodb->dispense( $table );
186: $results[] = $this->associateBeans( $bean1, $bean2, $bean );
187: }
188: }
189:
190: return ( count( $results ) > 1 ) ? $results : reset( $results );
191: }
192:
193: /**
194: * Counts the number of related beans in an N-M relation.
195: * This method returns the number of beans of type $type associated
196: * with reference bean(s) $bean. The query can be tuned using an
197: * SQL snippet for additional filtering.
198: *
199: * @param OODBBean|array $bean a bean object or an array of beans
200: * @param string $type type of bean you're interested in
201: * @param string $sql SQL snippet (optional)
202: * @param array $bindings bindings for your SQL string
203: *
204: * @return integer
205: */
206: public function relatedCount( $bean, $type, $sql = NULL, $bindings = array() )
207: {
208: if ( !( $bean instanceof OODBBean ) ) {
209: throw new RedException(
210: 'Expected array or OODBBean but got:' . gettype( $bean )
211: );
212: }
213:
214: if ( !$bean->id ) {
215: return 0;
216: }
217:
218: $beanType = $bean->getMeta( 'type' );
219:
220: try {
221: return $this->writer->queryRecordCountRelated( $beanType, $type, $bean->id, $sql, $bindings );
222: } catch ( SQLException $exception ) {
223: $this->handleException( $exception );
224:
225: return 0;
226: }
227: }
228:
229: /**
230: * Breaks the association between two beans. This method unassociates two beans. If the
231: * method succeeds the beans will no longer form an association. In the database
232: * this means that the association record will be removed. This method uses the
233: * OODB trash() method to remove the association links, thus giving FUSE models the
234: * opportunity to hook-in additional business logic. If the $fast parameter is
235: * set to boolean TRUE this method will remove the beans without their consent,
236: * bypassing FUSE. This can be used to improve performance.
237: *
238: * @param OODBBean $bean1 first bean in target association
239: * @param OODBBean $bean2 second bean in target association
240: * @param boolean $fast if TRUE, removes the entries by query without FUSE
241: *
242: * @return void
243: */
244: public function unassociate( $beans1, $beans2, $fast = NULL )
245: {
246: $beans1 = ( !is_array( $beans1 ) ) ? array( $beans1 ) : $beans1;
247: $beans2 = ( !is_array( $beans2 ) ) ? array( $beans2 ) : $beans2;
248:
249: foreach ( $beans1 as $bean1 ) {
250: foreach ( $beans2 as $bean2 ) {
251: try {
252: $this->oodb->store( $bean1 );
253: $this->oodb->store( $bean2 );
254:
255: $type1 = $bean1->getMeta( 'type' );
256: $type2 = $bean2->getMeta( 'type' );
257:
258: $row = $this->writer->queryRecordLink( $type1, $type2, $bean1->id, $bean2->id );
259: $linkType = $this->getTable( array( $type1, $type2 ) );
260:
261: if ( $fast ) {
262: $this->writer->deleteRecord( $linkType, array( 'id' => $row['id'] ) );
263:
264: return;
265: }
266:
267: $beans = $this->oodb->convertToBeans( $linkType, array( $row ) );
268:
269: if ( count( $beans ) > 0 ) {
270: $bean = reset( $beans );
271: $this->oodb->trash( $bean );
272: }
273: } catch ( SQLException $exception ) {
274: $this->handleException( $exception );
275: }
276: }
277: }
278: }
279:
280: /**
281: * Removes all relations for a bean. This method breaks every connection between
282: * a certain bean $bean and every other bean of type $type. Warning: this method
283: * is really fast because it uses a direct SQL query however it does not inform the
284: * models about this. If you want to notify FUSE models about deletion use a foreach-loop
285: * with unassociate() instead. (that might be slower though)
286: *
287: * @param OODBBean $bean reference bean
288: * @param string $type type of beans that need to be unassociated
289: *
290: * @return void
291: */
292: public function clearRelations( OODBBean $bean, $type )
293: {
294: $this->oodb->store( $bean );
295: try {
296: $this->writer->deleteRelations( $bean->getMeta( 'type' ), $type, $bean->id );
297: } catch ( SQLException $exception ) {
298: $this->handleException( $exception );
299: }
300: }
301:
302: /**
303: * Returns all the beans associated with $bean.
304: * This method will return an array containing all the beans that have
305: * been associated once with the associate() function and are still
306: * associated with the bean specified. The type parameter indicates the
307: * type of beans you are looking for. You can also pass some extra SQL and
308: * values for that SQL to filter your results after fetching the
309: * related beans.
310: *
311: * Don't try to make use of subqueries, a subquery using IN() seems to
312: * be slower than two queries!
313: *
314: * Since 3.2, you can now also pass an array of beans instead just one
315: * bean as the first parameter.
316: *
317: * @param OODBBean|array $bean the bean you have
318: * @param string $type the type of beans you want
319: * @param string $sql SQL snippet for extra filtering
320: * @param array $bindings values to be inserted in SQL slots
321: *
322: * @return array
323: */
324: public function related( $bean, $type, $sql = '', $bindings = array() )
325: {
326: $sql = $this->writer->glueSQLCondition( $sql );
327: $rows = $this->relatedRows( $bean, $type, $sql, $bindings );
328: $links = array();
329:
330: foreach ( $rows as $key => $row ) {
331: if ( !isset( $links[$row['id']] ) ) $links[$row['id']] = array();
332: $links[$row['id']][] = $row['linked_by'];
333: unset( $rows[$key]['linked_by'] );
334: }
335:
336: $beans = $this->oodb->convertToBeans( $type, $rows );
337: foreach ( $beans as $bean ) $bean->setMeta( 'sys.belongs-to', $links[$bean->id] );
338:
339: return $beans;
340: }
341: }
342: