Projet

Général

Profil

Paste
Télécharger (27 ko) Statistiques
| Branche: | Révision:

root / drupal7 / sites / all / modules / rules / includes / rules.state.inc @ 76e2e7c3

1
<?php
2

    
3
/**
4
 * @file Contains the state and data related stuff.
5
 */
6

    
7
/**
8
 * The rules evaluation state.
9
 *
10
 * A rule element may clone the state, so any added variables are only visible
11
 * for elements in the current PHP-variable-scope.
12
 */
13
class RulesState {
14

    
15
  /**
16
   * Globally keeps the ids of rules blocked due to recursion prevention.
17
   */
18
  static protected $blocked = array();
19

    
20
  /**
21
   * The known variables.
22
   */
23
  public $variables = array();
24

    
25
  /**
26
   * Holds info about the variables.
27
   */
28
  protected $info = array();
29

    
30
  /**
31
   * Keeps wrappers to be saved later on.
32
   */
33
  protected $save;
34

    
35
  /**
36
   * Holds the arguments while an element is executed. May be used by the
37
   * element to easily access the wrapped arguments.
38
   */
39
  public $currentArguments;
40

    
41
  /**
42
   * Variable for saving currently blocked configs for serialization.
43
   */
44
  protected $currentlyBlocked;
45

    
46

    
47
  public function __construct() {
48
    // Use an object in order to ensure any cloned states reference the same
49
    // save information.
50
    $this->save = new ArrayObject();
51
    $this->addVariable('site', FALSE, self::defaultVariables('site'));
52
  }
53

    
54
  /**
55
   * Adds the given variable to the given execution state.
56
   */
57
  public function addVariable($name, $data, $info) {
58
    $this->info[$name] = $info + array(
59
      'skip save' => FALSE,
60
      'type' => 'unknown',
61
      'handler' => FALSE,
62
    );
63
    if (empty($this->info[$name]['handler'])) {
64
      $this->variables[$name] = rules_wrap_data($data, $this->info[$name]);
65
    }
66
  }
67

    
68
  /**
69
   * Runs post-evaluation tasks, such as saving variables.
70
   */
71
  public function cleanUp() {
72
    // Make changes permanent.
73
    foreach ($this->save->getArrayCopy() as $selector => $wrapper) {
74
      $this->saveNow($selector);
75
    }
76
    unset($this->currentArguments);
77
  }
78

    
79
  /**
80
   * Block a rules configuration from execution.
81
   */
82
  public function block($rules_config) {
83
    if (empty($rules_config->recursion) && $rules_config->id) {
84
      self::$blocked[$rules_config->id] = TRUE;
85
    }
86
  }
87

    
88
  /**
89
   * Unblock a rules configuration from execution.
90
   */
91
  public function unblock($rules_config) {
92
    if (empty($rules_config->recursion) && $rules_config->id) {
93
      unset(self::$blocked[$rules_config->id]);
94
    }
95
  }
96

    
97
  /**
98
   * Returns whether a rules configuration should be blocked from execution.
99
   */
100
  public function isBlocked($rule_config) {
101
    return !empty($rule_config->id) && isset(self::$blocked[$rule_config->id]);
102
  }
103

    
104
  /**
105
   * Get the info about the state variables or a single variable.
106
   */
107
  public function varInfo($name = NULL) {
108
    if (isset($name)) {
109
      return isset($this->info[$name]) ? $this->info[$name] : FALSE;
110
    }
111
    return $this->info;
112
  }
113

    
114
  /**
115
   * Returns whether the given wrapper is savable.
116
   */
117
  public function isSavable($wrapper) {
118
    return ($wrapper instanceof EntityDrupalWrapper && entity_type_supports($wrapper->type(), 'save')) || $wrapper instanceof RulesDataWrapperSavableInterface;
119
  }
120

    
121
  /**
122
   * Returns whether the variable with the given name is an entity.
123
   */
124
  public function isEntity($name) {
125
    $entity_info = entity_get_info();
126
    return isset($this->info[$name]['type']) && isset($entity_info[$this->info[$name]['type']]);
127
  }
128

    
129
  /**
130
   * Gets a variable.
131
   *
132
   * If necessary, the specified handler is invoked to fetch the variable.
133
   *
134
   * @param $name
135
   *   The name of the variable to return.
136
   *
137
   * @return
138
   *   The variable or a EntityMetadataWrapper containing the variable.
139
   *
140
   * @throws RulesEvaluationException
141
   *   Throws a RulesEvaluationException in case we have info about the
142
   *   requested variable, but it is not defined.
143
   */
144
  public function &get($name) {
145
    if (!array_key_exists($name, $this->variables)) {
146
      // If there is handler to load the variable, do it now.
147
      if (!empty($this->info[$name]['handler'])) {
148
        $data = call_user_func($this->info[$name]['handler'], rules_unwrap_data($this->variables), $name, $this->info[$name]);
149
        $this->variables[$name] = rules_wrap_data($data, $this->info[$name]);
150
        $this->info[$name]['handler'] = FALSE;
151
        if (!isset($data)) {
152
          throw new RulesEvaluationException('Unable to load variable %name, aborting.', array('%name' => $name), NULL, RulesLog::INFO);
153
        }
154
      }
155
      else {
156
        throw new RulesEvaluationException('Unable to get variable %name, it is not defined.', array('%name' => $name), NULL, RulesLog::ERROR);
157
      }
158
    }
159
    return $this->variables[$name];
160
  }
161

    
162
  /**
163
   * Apply permanent changes provided the wrapper's data type is savable.
164
   *
165
   * @param $selector
166
   *   The data selector of the wrapper to save or just a variable name.
167
   * @param $immediate
168
   *   Pass FALSE to postpone saving to later on. Else it's immediately saved.
169
   */
170
  public function saveChanges($selector, $wrapper, $immediate = FALSE) {
171
    $info = $wrapper->info();
172
    if (empty($info['skip save']) && $this->isSavable($wrapper)) {
173
      $this->save($selector, $wrapper, $immediate);
174
    }
175
    // No entity, so try saving the parent.
176
    elseif (empty($info['skip save']) && isset($info['parent']) && !($wrapper instanceof EntityDrupalWrapper)) {
177
      // Cut of the last part of the selector.
178
      $selector = implode(':', explode(':', $selector, -1));
179
      $this->saveChanges($selector, $info['parent'], $immediate);
180
    }
181
    return $this;
182
  }
183

    
184
  /**
185
   * Remembers to save the wrapper on cleanup or does it now.
186
   */
187
  protected function save($selector, EntityMetadataWrapper $wrapper, $immediate) {
188
    // Convert variable names and selectors to both use underscores.
189
    $selector = strtr($selector, '-', '_');
190
    if (isset($this->save[$selector])) {
191
      if ($this->save[$selector][0]->getIdentifier() == $wrapper->getIdentifier()) {
192
        // The entity is already remembered. So do a combined save.
193
        $this->save[$selector][1] += self::$blocked;
194
      }
195
      else {
196
        // The wrapper is already in there, but wraps another entity. So first
197
        // save the old one, then care about the new one.
198
        $this->saveNow($selector);
199
      }
200
    }
201
    if (!isset($this->save[$selector])) {
202
      // In case of immediate saving don't clone the wrapper, so saving a new
203
      // entity immediately makes the identifier available afterwards.
204
      $this->save[$selector] = array($immediate ? $wrapper : clone $wrapper, self::$blocked);
205
    }
206
    if ($immediate) {
207
      $this->saveNow($selector);
208
    }
209
  }
210

    
211
  /**
212
   * Saves the wrapper for the given selector.
213
   */
214
  protected function saveNow($selector) {
215
    // Add the set of blocked elements for the recursion prevention.
216
    $previously_blocked = self::$blocked;
217
    self::$blocked += $this->save[$selector][1];
218

    
219
    // Actually save!
220
    $wrapper = $this->save[$selector][0];
221
    $entity = $wrapper->value();
222
    // When operating in hook_entity_insert() $entity->is_new might be still
223
    // set. In that case remove the flag to avoid causing another insert instead
224
    // of an update.
225
    if (!empty($entity->is_new) && $wrapper->getIdentifier()) {
226
      $entity->is_new = FALSE;
227
    }
228
    rules_log('Saved %selector of type %type.', array('%selector' => $selector, '%type' => $wrapper->type()));
229
    $wrapper->save();
230

    
231
    // Restore the state's set of blocked elements.
232
    self::$blocked = $previously_blocked;
233
    unset($this->save[$selector]);
234
  }
235

    
236
  /**
237
   * Merges the info about to be saved variables form the given state into the
238
   * existing state. Therefor we can aggregate saves from invoked components.
239
   * Merged in saves are removed from the given state, but not mergable saves
240
   * remain there.
241
   *
242
   * @param $state
243
   *   The state for which to merge the to be saved variables in.
244
   * @param $component
245
   *   The component which has been invoked, thus needs to be blocked for the
246
   *   merged in saves.
247
   * @param $settings
248
   *   The settings of the element that invoked the component. Contains
249
   *   information about variable/selector mappings between the states.
250
   */
251
  public function mergeSaveVariables(RulesState $state, RulesPlugin $component, $settings) {
252
    // For any saves that we take over, also block the component.
253
    $this->block($component);
254

    
255
    foreach ($state->save->getArrayCopy() as $selector => $data) {
256
      $parts = explode(':', $selector, 2);
257
      // Adapt the selector to fit for the parent state and move the wrapper.
258
      if (isset($settings[$parts[0] . ':select'])) {
259
        $parts[0] = $settings[$parts[0] . ':select'];
260
        $this->save(implode(':', $parts), $data[0], FALSE);
261
        unset($state->save[$selector]);
262
      }
263
    }
264
    $this->unblock($component);
265
  }
266

    
267
  /**
268
   * Returns an entity metadata wrapper as specified in the selector.
269
   *
270
   * @param $selector
271
   *   The selector string, e.g. "node:author:mail".
272
   * @param $langcode
273
   *   (optional) The language code used to get the argument value if the
274
   *   argument value should be translated. Defaults to LANGUAGE_NONE.
275
   *
276
   * @return EntityMetadataWrapper
277
   *   The wrapper for the given selector.
278
   *
279
   * @throws RulesEvaluationException
280
   *   Throws a RulesEvaluationException in case the selector cannot be applied.
281
   */
282
  public function applyDataSelector($selector, $langcode = LANGUAGE_NONE) {
283
    $parts = explode(':', str_replace('-', '_', $selector), 2);
284
    $wrapper = $this->get($parts[0]);
285
    if (count($parts) == 1) {
286
      return $wrapper;
287
    }
288
    elseif (!$wrapper instanceof EntityMetadataWrapper) {
289
      throw new RulesEvaluationException('Unable to apply data selector %selector. The specified variable is not wrapped correctly.', array('%selector' => $selector));
290
    }
291
    try {
292
      foreach (explode(':', $parts[1]) as $name) {
293
        if ($wrapper instanceof EntityListWrapper || $wrapper instanceof EntityStructureWrapper) {
294
          // Make sure we are usign the right language. Wrappers might be cached
295
          // and have previous langcodes set, so always set the right language.
296
          if ($wrapper instanceof EntityStructureWrapper) {
297
            $wrapper->language($langcode);
298
          }
299
          $wrapper = $wrapper->get($name);
300
        }
301
        else {
302
          throw new RulesEvaluationException('Unable to apply data selector %selector. The specified variable is not a list or a structure: %wrapper.', array('%selector' => $selector, '%wrapper' => $wrapper));
303
        }
304
      }
305
    }
306
    catch (EntityMetadataWrapperException $e) {
307
      // In case of an exception, re-throw it.
308
      throw new RulesEvaluationException('Unable to apply data selector %selector: %error', array('%selector' => $selector, '%error' => $e->getMessage()));
309
    }
310
    return $wrapper;
311
  }
312

    
313
  /**
314
   * Magic method. Only serialize variables and their info.
315
   * Additionally we remember currently blocked configs, so we can restore them
316
   * upon deserialization using restoreBlocks().
317
   */
318
  public function __sleep () {
319
    $this->currentlyBlocked = self::$blocked;
320
    return array('info', 'variables', 'currentlyBlocked');
321
  }
322

    
323
  public function __wakeup() {
324
    $this->save = new ArrayObject();
325
  }
326

    
327
  /**
328
   * Restore the before serialization blocked configurations.
329
   *
330
   * Warning: This overwrites any possible currently blocked configs. Thus
331
   * do not invoke this method, if there might be evaluations active.
332
   */
333
  public function restoreBlocks() {
334
    self::$blocked = $this->currentlyBlocked;
335
  }
336

    
337
  /**
338
   * Defines always available variables.
339
   */
340
  public static function defaultVariables($key = NULL) {
341
    // Add a variable for accessing site-wide data properties.
342
    $vars['site'] = array(
343
      'type' => 'site',
344
      'label' => t('Site information'),
345
      'description' => t("Site-wide settings and other global information."),
346
      // Add the property info via a callback making use of the cached info.
347
      'property info alter' => array('RulesData', 'addSiteMetadata'),
348
      'property info' => array(),
349
      'optional' => TRUE,
350
    );
351
    return isset($key) ? $vars[$key] : $vars;
352
  }
353
}
354

    
355
/**
356
 * A class holding static methods related to data.
357
 */
358
class RulesData  {
359

    
360
  /**
361
   * Returns whether the type match. They match if type1 is compatible to type2.
362
   *
363
   * @param $var_info
364
   *   The name of the type to check for whether it is compatible to type2.
365
   * @param $param_info
366
   *   The type expression to check for.
367
   * @param $ancestors
368
   *   Whether sub-type relationships for checking type compatibility should be
369
   *   taken into account. Defaults to TRUE.
370
   *
371
   * @return
372
   *   Whether the types match.
373
   */
374
  public static function typesMatch($var_info, $param_info, $ancestors = TRUE) {
375
    $var_type = $var_info['type'];
376
    $param_type = $param_info['type'];
377

    
378
    if ($param_type == '*' || $param_type == 'unknown') {
379
      return TRUE;
380
    }
381

    
382
    if ($var_type == $param_type) {
383
      // Make sure the bundle matches, if specified by the parameter.
384
      return !isset($param_info['bundles']) || isset($var_info['bundle']) && in_array($var_info['bundle'], $param_info['bundles']);
385
    }
386

    
387
    // Parameters may specify multiple types using an array.
388
    $valid_types = is_array($param_type) ? $param_type : array($param_type);
389
    if (in_array($var_type, $valid_types)) {
390
      return TRUE;
391
    }
392

    
393
    // Check for sub-type relationships.
394
    if ($ancestors && !isset($param_info['bundles'])) {
395
      $cache = &rules_get_cache();
396
      self::typeCalcAncestors($cache, $var_type);
397
      // If one of the types is an ancestor return TRUE.
398
      return (bool)array_intersect_key($cache['data_info'][$var_type]['ancestors'], array_flip($valid_types));
399
    }
400
    return FALSE;
401
  }
402

    
403
  protected static function typeCalcAncestors(&$cache, $type) {
404
    if (!isset($cache['data_info'][$type]['ancestors'])) {
405
      $cache['data_info'][$type]['ancestors'] = array();
406
      if (isset($cache['data_info'][$type]['parent']) && $parent = $cache['data_info'][$type]['parent']) {
407
        $cache['data_info'][$type]['ancestors'][$parent] = TRUE;
408
        self::typeCalcAncestors($cache, $parent);
409
        // Add all parent ancestors to our own ancestors.
410
        $cache['data_info'][$type]['ancestors'] += $cache['data_info'][$parent]['ancestors'];
411
      }
412
      // For special lists like list<node> add in "list" as valid parent.
413
      if (entity_property_list_extract_type($type)) {
414
        $cache['data_info'][$type]['ancestors']['list'] = TRUE;
415
      }
416
    }
417
  }
418

    
419
  /**
420
   * Returns matching data variables or properties for the given info and the to
421
   * be configured parameter.
422
   *
423
   * @param $source
424
   *   Either an array of info about available variables or a entity metadata
425
   *   wrapper.
426
   * @param $param_info
427
   *   The information array about the to be configured parameter.
428
   * @param $prefix
429
   *   An optional prefix for the data selectors.
430
   * @param $recursions
431
   *   The number of recursions used to go down the tree. Defaults to 2.
432
   * @param $suggestions
433
   *   Whether possibilities to recurse are suggested as soon as the deepest
434
   *   level of recursions is reached. Defaults to TRUE.
435
   *
436
   * @return
437
   *  An array of info about matching variables or properties that match, keyed
438
   *  with the data selector.
439
   */
440
  public static function matchingDataSelector($source, $param_info, $prefix = '', $recursions = 2, $suggestions = TRUE) {
441
    // If an array of info is given, get entity metadata wrappers first.
442
    $data = NULL;
443
    if (is_array($source)) {
444
      foreach ($source as $name => $info) {
445
        $source[$name] = rules_wrap_data($data, $info, TRUE);
446
      }
447
    }
448

    
449
    $matches = array();
450
    foreach ($source as $name => $wrapper) {
451
      $info = $wrapper->info();
452
      $name = str_replace('_', '-', $name);
453

    
454
      if (self::typesMatch($info, $param_info)) {
455
        $matches[$prefix . $name] = $info;
456
        if (!is_array($source) && $source instanceof EntityListWrapper) {
457
          // Add some more possible list items.
458
          for ($i = 1; $i < 4; $i++) {
459
            $matches[$prefix . $i] = $info;
460
          }
461
        }
462
      }
463
      // Recurse later on to get an improved ordering of the results.
464
      if ($wrapper instanceof EntityStructureWrapper || $wrapper instanceof EntityListWrapper) {
465
        $recurse[$prefix . $name] = $wrapper;
466
        if ($recursions > 0) {
467
          $matches += self::matchingDataSelector($wrapper, $param_info, $prefix . $name . ':', $recursions - 1, $suggestions);
468
        }
469
        elseif ($suggestions) {
470
          // We may not recurse any more, but indicate the possibility to recurse.
471
          $matches[$prefix . $name . ':'] = $wrapper->info();
472
          if (!is_array($source) && $source instanceof EntityListWrapper) {
473
            // Add some more possible list items.
474
            for ($i = 1; $i < 4; $i++) {
475
              $matches[$prefix . $i . ':'] = $wrapper->info();
476
            }
477
          }
478
        }
479
      }
480
    }
481
    return $matches;
482
  }
483

    
484
  /**
485
   * Adds asserted metadata to the variable info. In case there are already
486
   * assertions for a variable, the assertions are merged such that both apply.
487
   *
488
   * @see RulesData::applyMetadataAssertions()
489
   */
490
  public static function addMetadataAssertions($var_info, $assertions) {
491
    foreach ($assertions as $selector => $assertion) {
492
      // Convert the selector back to underscores, such it matches the varname.
493
      $selector = str_replace('-', '_', $selector);
494

    
495
      $parts = explode(':', $selector);
496
      if (isset($var_info[$parts[0]])) {
497
        // Apply the selector to determine the right target array. We build an
498
        // array like
499
        // $var_info['rules assertion']['property1']['property2']['#info'] = ..
500
        $target = &$var_info[$parts[0]]['rules assertion'];
501
        foreach (array_slice($parts, 1) as $part) {
502
          $target = &$target[$part];
503
        }
504

    
505
        // In case the assertion is directly for a variable, we have to modify
506
        // the variable info directly. In case the asserted property is nested
507
        // the info-has to be altered by RulesData::applyMetadataAssertions()
508
        // before the child-wrapper is created.
509
        if (count($parts) == 1) {
510
          // Support asserting a type in case of generic entity references only.
511
          if (isset($assertion['type']) && $var_info[$parts[0]]['type'] == 'entity') {
512
            if (entity_get_info($assertion['type'])) {
513
              $var_info[$parts[0]]['type'] = $assertion['type'];
514
            }
515
            unset($assertion['type']);
516
          }
517
          // Add any single bundle directly to the variable info, so the
518
          // variable fits as argument for parameters requiring the bundle.
519
          if (isset($assertion['bundle']) && count($bundles = (array) $assertion['bundle']) == 1) {
520
            $var_info[$parts[0]]['bundle'] = reset($bundles);
521
          }
522
        }
523

    
524
        // Add the assertions, but merge them with any previously added
525
        // assertions if necessary.
526
        $target['#info'] = isset($target['#info']) ? rules_update_array($target['#info'], $assertion) : $assertion;
527

    
528
        // Add in a callback that the entity metadata wrapper pick up for
529
        // altering the property info, such that we can add in the assertions.
530
        $var_info[$parts[0]] += array('property info alter' => array('RulesData', 'applyMetadataAssertions'));
531

    
532
        // In case there is a VARNAME_unchanged variable as it is used in update
533
        // hooks, assume the assertions are valid for the unchanged variable
534
        // too.
535
        if (isset($var_info[$parts[0] . '_unchanged'])) {
536
          $name = $parts[0] . '_unchanged';
537
          $var_info[$name]['rules assertion'] = $var_info[$parts[0]]['rules assertion'];
538
          $var_info[$name]['property info alter'] = array('RulesData', 'applyMetadataAssertions');
539

    
540
          if (isset($var_info[$parts[0]]['bundle']) && !isset($var_info[$name]['bundle'])) {
541
            $var_info[$name]['bundle'] = $var_info[$parts[0]]['bundle'];
542
          }
543
        }
544
      }
545
    }
546
    return $var_info;
547
  }
548

    
549
  /**
550
   * Property info alter callback for the entity metadata wrapper for applying
551
   * the rules metadata assertions.
552
   *
553
   * @see RulesData::addMetadataAssertions()
554
   */
555
  public static function applyMetadataAssertions(EntityMetadataWrapper $wrapper, $property_info) {
556
    $info = $wrapper->info();
557

    
558
    if (!empty($info['rules assertion'])) {
559
      $assertion = $info['rules assertion'];
560

    
561
      // In case there are list-wrappers pass through the assertions of the item
562
      // but make sure we only apply the assertions for the list items for
563
      // which the conditions are executed.
564
      if (isset($info['parent']) && $info['parent'] instanceof EntityListWrapper) {
565
        $assertion = isset($assertion[$info['name']]) ? $assertion[$info['name']] : array();
566
      }
567

    
568
      // Support specifying multiple bundles, whereas the added properties are
569
      // the intersection of the bundle properties.
570
      if (isset($assertion['#info']['bundle'])) {
571
        $bundles = (array) $assertion['#info']['bundle'];
572
        foreach ($bundles as $bundle) {
573
          $properties[] = isset($property_info['bundles'][$bundle]['properties']) ? $property_info['bundles'][$bundle]['properties'] : array();
574
        }
575
        // Add the intersection.
576
        $property_info['properties'] += count($properties) > 1 ? call_user_func_array('array_intersect_key', $properties) : reset($properties);
577
      }
578
      // Support adding directly asserted property info.
579
      if (isset($assertion['#info']['property info'])) {
580
        $property_info['properties'] += $assertion['#info']['property info'];
581
      }
582

    
583
      // Pass through any rules assertion of properties to their info, so any
584
      // derived wrappers apply it.
585
      foreach (element_children($assertion) as $key) {
586
        $property_info['properties'][$key]['rules assertion'] = $assertion[$key];
587
        $property_info['properties'][$key]['property info alter'] = array('RulesData', 'applyMetadataAssertions');
588

    
589
        // Apply any 'type' and 'bundle' assertion directly to the propertyinfo.
590
        if (isset($assertion[$key]['#info']['type'])) {
591
          $type = $assertion[$key]['#info']['type'];
592
          // Support asserting a type in case of generic entity references only.
593
          if ($property_info['properties'][$key]['type'] == 'entity' && entity_get_info($type)) {
594
            $property_info['properties'][$key]['type'] = $type;
595
          }
596
        }
597
        if (isset($assertion[$key]['#info']['bundle'])) {
598
          $bundle = (array) $assertion[$key]['#info']['bundle'];
599
          // Add any single bundle directly to the variable info, so the
600
          // property fits as argument for parameters requiring the bundle.
601
          if (count($bundle) == 1) {
602
            $property_info['properties'][$key]['bundle'] = reset($bundle);
603
          }
604
        }
605
      }
606
    }
607
    return $property_info;
608
  }
609

    
610
  /**
611
   * Property info alter callback for the entity metadata wrapper to inject
612
   * metadata for the 'site' variable. In contrast to doing this via
613
   * hook_rules_data_info() this callback makes use of the already existing
614
   * property info cache for site information of entity metadata.
615
   *
616
   * @see RulesPlugin::availableVariables()
617
   */
618
  public static function addSiteMetadata(EntityMetadataWrapper $wrapper, $property_info) {
619
    $site_info = entity_get_property_info('site');
620
    $property_info['properties'] += $site_info['properties'];
621
    // Also invoke the usual callback for altering metadata, in case actions
622
    // have specified further metadata.
623
    return RulesData::applyMetadataAssertions($wrapper, $property_info);
624
  }
625
}
626

    
627
/**
628
 * A wrapper class similar to the EntityDrupalWrapper, but for non-entities.
629
 *
630
 * This class is intended to serve as base for a custom wrapper classes of
631
 * identifiable data types, which are non-entities. By extending this class only
632
 * the extractIdentifier() and load() methods have to be defined.
633
 * In order to make the data type savable implement the
634
 * RulesDataWrapperSavableInterface.
635
 *
636
 * That way it is possible for non-entity data types to be work with Rules, i.e.
637
 * one can implement a 'ui class' with a direct input form returning the
638
 * identifier of the data. However, instead of that it is suggested to implement
639
 * an entity type, such that the same is achieved via general API functions like
640
 * entity_load().
641
 */
642
abstract class RulesIdentifiableDataWrapper extends EntityStructureWrapper {
643

    
644
  /**
645
   * Contains the id.
646
   */
647
  protected $id = FALSE;
648

    
649
  /**
650
   * Construct a new wrapper object.
651
   *
652
   * @param $type
653
   *   The type of the passed data.
654
   * @param $data
655
   *   Optional. The data to wrap or its identifier.
656
   * @param $info
657
   *   Optional. Used internally to pass info about properties down the tree.
658
   */
659
  public function __construct($type, $data = NULL, $info = array()) {
660
    parent::__construct($type, $data, $info);
661
    $this->setData($data);
662
  }
663

    
664
  /**
665
   * Sets the data internally accepting both the data id and object.
666
   */
667
  protected function setData($data) {
668
    if (isset($data) && $data !== FALSE && !is_object($data)) {
669
      $this->id = $data;
670
      $this->data = FALSE;
671
    }
672
    elseif (is_object($data)) {
673
      // We got the data object passed.
674
      $this->data = $data;
675
      $id = $this->extractIdentifier($data);
676
      $this->id = isset($id) ? $id : FALSE;
677
    }
678
  }
679

    
680
  /**
681
   * Returns the identifier of the wrapped data.
682
   */
683
  public function getIdentifier() {
684
    return $this->dataAvailable() && $this->value() ? $this->id : NULL;
685
  }
686

    
687
  /**
688
   * Overridden.
689
   */
690
  public function value(array $options = array()) {
691
    $this->setData(parent::value());
692
    if (!$this->data && !empty($this->id)) {
693
      // Lazy load the data if necessary.
694
      $this->data = $this->load($this->id);
695
      if (!$this->data) {
696
        throw new EntityMetadataWrapperException('Unable to load the ' . check_plain($this->type) . ' with the id ' . check_plain($this->id) . '.');
697
      }
698
    }
699
    return $this->data;
700
  }
701

    
702
  /**
703
   * Overridden to support setting the data by either the object or the id.
704
   */
705
  public function set($value) {
706
    if (!$this->validate($value)) {
707
      throw new EntityMetadataWrapperException('Invalid data value given. Be sure it matches the required data type and format.');
708
    }
709
    // As custom wrapper classes can only appear for Rules variables, but not
710
    // as properties we don't have to care about updating the parent.
711
    $this->clear();
712
    $this->setData($value);
713
    return $this;
714
  }
715

    
716
  /**
717
   * Overridden.
718
   */
719
  public function clear() {
720
    $this->id = NULL;
721
    parent::clear();
722
  }
723

    
724
  /**
725
   * Prepare for serializiation.
726
   */
727
  public function __sleep() {
728
    $vars = parent::__sleep();
729
    // Don't serialize the loaded data, except for the case the data is not
730
    // saved yet.
731
    if (!empty($this->id)) {
732
      unset($vars['data']);
733
    }
734
    return $vars;
735
  }
736

    
737
  public function __wakeup() {
738
    if ($this->id !== FALSE) {
739
      // Make sure data is set, so the data will be loaded when needed.
740
      $this->data = FALSE;
741
    }
742
  }
743

    
744
  /**
745
   * Extract the identifier of the given data object.
746
   *
747
   * @return
748
   *   The extracted identifier.
749
   */
750
  abstract protected function extractIdentifier($data);
751

    
752
  /**
753
   * Load a data object given an identifier.
754
   *
755
   * @return
756
   *   The loaded data object, or FALSE if loading failed.
757
   */
758
  abstract protected function load($id);
759
}
760

    
761
/**
762
 * Interface that allows custom wrapper classes to declare that they are savable.
763
 */
764
interface RulesDataWrapperSavableInterface {
765

    
766
  /**
767
   * Save the currently wrapped data.
768
   */
769
  public function save();
770
}