Projet

Général

Profil

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

root / drupal7 / sites / all / modules / rules / includes / rules.state.inc @ 950416da

1
<?php
2

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

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

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

    
23
  /**
24
   * The known variables.
25
   *
26
   * @var array
27
   */
28
  public $variables = array();
29

    
30
  /**
31
   * Holds info about the variables.
32
   *
33
   * @var array
34
   */
35
  protected $info = array();
36

    
37
  /**
38
   * Keeps wrappers to be saved later on.
39
   */
40
  protected $save;
41

    
42
  /**
43
   * Holds the arguments while an element is executed.
44
   *
45
   * May be used by the element to easily access the wrapped arguments.
46
   */
47
  public $currentArguments;
48

    
49
  /**
50
   * Variable for saving currently blocked configs for serialization.
51
   */
52
  protected $currentlyBlocked;
53

    
54
  /**
55
   * Constructs a RulesState object.
56
   */
57
  public function __construct() {
58
    // Use an object in order to ensure any cloned states reference the same
59
    // save information.
60
    $this->save = new ArrayObject();
61
    $this->addVariable('site', FALSE, self::defaultVariables('site'));
62
  }
63

    
64
  /**
65
   * Adds the given variable to the given execution state.
66
   */
67
  public function addVariable($name, $data, $info) {
68
    $this->info[$name] = $info + array(
69
      'skip save' => FALSE,
70
      'type' => 'unknown',
71
      'handler' => FALSE,
72
    );
73
    if (empty($this->info[$name]['handler'])) {
74
      $this->variables[$name] = rules_wrap_data($data, $this->info[$name]);
75
    }
76
  }
77

    
78
  /**
79
   * Runs post-evaluation tasks, such as saving variables.
80
   */
81
  public function cleanUp() {
82
    // Make changes permanent.
83
    foreach ($this->save->getArrayCopy() as $selector => $wrapper) {
84
      $this->saveNow($selector);
85
    }
86
    unset($this->currentArguments);
87
  }
88

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

    
98
  /**
99
   * Unblock a rules configuration from execution.
100
   */
101
  public function unblock($rules_config) {
102
    if (empty($rules_config->recursion) && $rules_config->id) {
103
      unset(self::$blocked[$rules_config->id]);
104
    }
105
  }
106

    
107
  /**
108
   * Returns whether a rules configuration should be blocked from execution.
109
   */
110
  public function isBlocked($rule_config) {
111
    return !empty($rule_config->id) && isset(self::$blocked[$rule_config->id]);
112
  }
113

    
114
  /**
115
   * Get the info about the state variables or a single variable.
116
   */
117
  public function varInfo($name = NULL) {
118
    if (isset($name)) {
119
      return isset($this->info[$name]) ? $this->info[$name] : FALSE;
120
    }
121
    return $this->info;
122
  }
123

    
124
  /**
125
   * Returns whether the given wrapper is savable.
126
   */
127
  public function isSavable($wrapper) {
128
    return ($wrapper instanceof EntityDrupalWrapper && entity_type_supports($wrapper->type(), 'save')) || $wrapper instanceof RulesDataWrapperSavableInterface;
129
  }
130

    
131
  /**
132
   * Returns whether the variable with the given name is an entity.
133
   */
134
  public function isEntity($name) {
135
    $entity_info = entity_get_info();
136
    return isset($this->info[$name]['type']) && isset($entity_info[$this->info[$name]['type']]);
137
  }
138

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

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

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

    
222
  /**
223
   * Saves the wrapper for the given selector.
224
   */
225
  protected function saveNow($selector) {
226
    // Add the set of blocked elements for the recursion prevention.
227
    $previously_blocked = self::$blocked;
228
    self::$blocked += $this->save[$selector][1];
229

    
230
    // Actually save!
231
    $wrapper = $this->save[$selector][0];
232
    $entity = $wrapper->value();
233
    // When operating in hook_entity_insert() $entity->is_new might be still
234
    // set. In that case remove the flag to avoid causing another insert instead
235
    // of an update.
236
    if (!empty($entity->is_new) && $wrapper->getIdentifier()) {
237
      $entity->is_new = FALSE;
238
    }
239
    rules_log('Saved %selector of type %type.', array('%selector' => $selector, '%type' => $wrapper->type()));
240
    $wrapper->save();
241

    
242
    // Restore the state's set of blocked elements.
243
    self::$blocked = $previously_blocked;
244
    unset($this->save[$selector]);
245
  }
246

    
247
  /**
248
   * Merges info from the given state into the existing state.
249
   *
250
   * Merges the info about to-be-saved variables from the given state into the
251
   * existing state. Therefore we can aggregate saves from invoked components.
252
   * Merged-in saves are removed from the given state, but not-mergeable saves
253
   * remain there.
254
   *
255
   * @param $state
256
   *   The state for which to merge the to be saved variables in.
257
   * @param $component
258
   *   The component which has been invoked, thus needs to be blocked for the
259
   *   merged in saves.
260
   * @param $settings
261
   *   The settings of the element that invoked the component. Contains
262
   *   information about variable/selector mappings between the states.
263
   */
264
  public function mergeSaveVariables(RulesState $state, RulesPlugin $component, $settings) {
265
    // For any saves that we take over, also block the component.
266
    $this->block($component);
267

    
268
    foreach ($state->save->getArrayCopy() as $selector => $data) {
269
      $parts = explode(':', $selector, 2);
270
      // Adapt the selector to fit for the parent state and move the wrapper.
271
      if (isset($settings[$parts[0] . ':select'])) {
272
        $parts[0] = $settings[$parts[0] . ':select'];
273
        $this->save(implode(':', $parts), $data[0], FALSE);
274
        unset($state->save[$selector]);
275
      }
276
    }
277
    $this->unblock($component);
278
  }
279

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

    
326
  /**
327
   * Magic method. Only serialize variables and their info.
328
   *
329
   * Additionally we remember currently blocked configs, so we can restore them
330
   * upon deserialization using restoreBlocks().
331
   */
332
  public function __sleep() {
333
    $this->currentlyBlocked = self::$blocked;
334
    return array('info', 'variables', 'currentlyBlocked');
335
  }
336

    
337
  /**
338
   * Magic method. Unserialize variables and their info.
339
   */
340
  public function __wakeup() {
341
    $this->save = new ArrayObject();
342
  }
343

    
344
  /**
345
   * Restores the before-serialization blocked configurations.
346
   *
347
   * Warning: This overwrites any possible currently blocked configs. Thus
348
   * do not invoke this method if there might be evaluations active.
349
   */
350
  public function restoreBlocks() {
351
    self::$blocked = $this->currentlyBlocked;
352
  }
353

    
354
  /**
355
   * Defines always-available variables.
356
   *
357
   * @param $key
358
   *   (optional)
359
   */
360
  public static function defaultVariables($key = NULL) {
361
    // Add a variable for accessing site-wide data properties.
362
    $vars['site'] = array(
363
      'type' => 'site',
364
      'label' => t('Site information'),
365
      'description' => t("Site-wide settings and other global information."),
366
      // Add the property info via a callback making use of the cached info.
367
      'property info alter' => array('RulesData', 'addSiteMetadata'),
368
      'property info' => array(),
369
      'optional' => TRUE,
370
    );
371
    return isset($key) ? $vars[$key] : $vars;
372
  }
373

    
374
}
375

    
376
/**
377
 * A class holding static methods related to data.
378
 */
379
class RulesData {
380

    
381
  /**
382
   * Returns whether the type match. They match if type1 is compatible to type2.
383
   *
384
   * @param $var_info
385
   *   The name of the type to check for whether it is compatible to type2.
386
   * @param $param_info
387
   *   The type expression to check for.
388
   * @param bool $ancestors
389
   *   (optional) Whether sub-type relationships for checking type compatibility
390
   *   should be taken into account. Defaults to TRUE.
391
   *
392
   * @return bool
393
   *   Whether the types match.
394
   */
395
  public static function typesMatch($var_info, $param_info, $ancestors = TRUE) {
396
    $var_type = $var_info['type'];
397
    $param_type = $param_info['type'];
398

    
399
    if ($param_type == '*' || $param_type == 'unknown') {
400
      return TRUE;
401
    }
402

    
403
    if ($var_type == $param_type) {
404
      // Make sure the bundle matches, if specified by the parameter.
405
      return !isset($param_info['bundles']) || isset($var_info['bundle']) && in_array($var_info['bundle'], $param_info['bundles']);
406
    }
407

    
408
    // Parameters may specify multiple types using an array.
409
    $valid_types = is_array($param_type) ? $param_type : array($param_type);
410
    if (in_array($var_type, $valid_types)) {
411
      return TRUE;
412
    }
413

    
414
    // Check for sub-type relationships.
415
    if ($ancestors && !isset($param_info['bundles'])) {
416
      $cache = &rules_get_cache();
417
      self::typeCalcAncestors($cache, $var_type);
418
      // If one of the types is an ancestor return TRUE.
419
      return (bool) array_intersect_key($cache['data_info'][$var_type]['ancestors'], array_flip($valid_types));
420
    }
421
    return FALSE;
422
  }
423

    
424
  protected static function typeCalcAncestors(&$cache, $type) {
425
    if (!isset($cache['data_info'][$type]['ancestors'])) {
426
      $cache['data_info'][$type]['ancestors'] = array();
427
      if (isset($cache['data_info'][$type]['parent']) && $parent = $cache['data_info'][$type]['parent']) {
428
        $cache['data_info'][$type]['ancestors'][$parent] = TRUE;
429
        self::typeCalcAncestors($cache, $parent);
430
        // Add all parent ancestors to our own ancestors.
431
        $cache['data_info'][$type]['ancestors'] += $cache['data_info'][$parent]['ancestors'];
432
      }
433
      // For special lists like list<node> add in "list" as valid parent.
434
      if (entity_property_list_extract_type($type)) {
435
        $cache['data_info'][$type]['ancestors']['list'] = TRUE;
436
      }
437
    }
438
  }
439

    
440
  /**
441
   * Returns data for the given info and the to-be-configured parameter.
442
   *
443
   * Returns matching data variables or properties for the given info and the
444
   * to-be-configured parameter.
445
   *
446
   * @param $source
447
   *   Either an array of info about available variables or a entity metadata
448
   *   wrapper.
449
   * @param $param_info
450
   *   The information array about the to be configured parameter.
451
   * @param string $prefix
452
   *   An optional prefix for the data selectors.
453
   * @param int $recursions
454
   *   The number of recursions used to go down the tree. Defaults to 2.
455
   * @param bool $suggestions
456
   *   Whether possibilities to recurse are suggested as soon as the deepest
457
   *   level of recursions is reached. Defaults to TRUE.
458
   *
459
   * @return array
460
   *   An array of info about matching variables or properties that match, keyed
461
   *   with the data selector.
462
   */
463
  public static function matchingDataSelector($source, $param_info, $prefix = '', $recursions = 2, $suggestions = TRUE) {
464
    // If an array of info is given, get entity metadata wrappers first.
465
    $data = NULL;
466
    if (is_array($source)) {
467
      foreach ($source as $name => $info) {
468
        $source[$name] = rules_wrap_data($data, $info, TRUE);
469
      }
470
    }
471

    
472
    $matches = array();
473
    foreach ($source as $name => $wrapper) {
474
      $info = $wrapper->info();
475
      $name = str_replace('_', '-', $name);
476

    
477
      if (self::typesMatch($info, $param_info)) {
478
        $matches[$prefix . $name] = $info;
479
        if (!is_array($source) && $source instanceof EntityListWrapper) {
480
          // Add some more possible list items.
481
          for ($i = 1; $i < 4; $i++) {
482
            $matches[$prefix . $i] = $info;
483
          }
484
        }
485
      }
486
      // Recurse later on to get an improved ordering of the results.
487
      if ($wrapper instanceof EntityStructureWrapper || $wrapper instanceof EntityListWrapper) {
488
        $recurse[$prefix . $name] = $wrapper;
489
        if ($recursions > 0) {
490
          $matches += self::matchingDataSelector($wrapper, $param_info, $prefix . $name . ':', $recursions - 1, $suggestions);
491
        }
492
        elseif ($suggestions) {
493
          // We may not recurse any more,
494
          // but indicate the possibility to recurse.
495
          $matches[$prefix . $name . ':'] = $wrapper->info();
496
          if (!is_array($source) && $source instanceof EntityListWrapper) {
497
            // Add some more possible list items.
498
            for ($i = 1; $i < 4; $i++) {
499
              $matches[$prefix . $i . ':'] = $wrapper->info();
500
            }
501
          }
502
        }
503
      }
504
    }
505
    return $matches;
506
  }
507

    
508
  /**
509
   * Adds asserted metadata to the variable info.
510
   *
511
   * In case there are already assertions for a variable, the assertions are
512
   * merged such that both apply.
513
   *
514
   * @see RulesData::applyMetadataAssertions()
515
   */
516
  public static function addMetadataAssertions($var_info, $assertions) {
517
    foreach ($assertions as $selector => $assertion) {
518
      // Convert the selector back to underscores, such it matches the varname.
519
      $selector = str_replace('-', '_', $selector);
520

    
521
      $parts = explode(':', $selector);
522
      if (isset($var_info[$parts[0]])) {
523
        // Apply the selector to determine the right target array. We build an
524
        // array like
525
        // $var_info['rules assertion']['property1']['property2']['#info'] = ..
526
        $target = &$var_info[$parts[0]]['rules assertion'];
527
        foreach (array_slice($parts, 1) as $part) {
528
          $target = &$target[$part];
529
        }
530

    
531
        // In case the assertion is directly for a variable, we have to modify
532
        // the variable info directly. In case the asserted property is nested
533
        // the info-has to be altered by RulesData::applyMetadataAssertions()
534
        // before the child-wrapper is created.
535
        if (count($parts) == 1) {
536
          // Support asserting a type in case of generic entity references only.
537
          $var_type = &$var_info[$parts[0]]['type'];
538
          if (isset($assertion['type']) && ($var_type == 'entity' || $var_type == 'list<entity>')) {
539
            $var_type = $assertion['type'];
540
            unset($assertion['type']);
541
          }
542
          // Add any single bundle directly to the variable info, so the
543
          // variable fits as argument for parameters requiring the bundle.
544
          if (isset($assertion['bundle']) && count($bundles = (array) $assertion['bundle']) == 1) {
545
            $var_info[$parts[0]]['bundle'] = reset($bundles);
546
          }
547
        }
548

    
549
        // Add the assertions, but merge them with any previously added
550
        // assertions if necessary.
551
        $target['#info'] = isset($target['#info']) ? rules_update_array($target['#info'], $assertion) : $assertion;
552

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

    
557
        // In case there is a VARNAME_unchanged variable as it is used in update
558
        // hooks, assume the assertions are valid for the unchanged variable
559
        // too.
560
        if (isset($var_info[$parts[0] . '_unchanged'])) {
561
          $name = $parts[0] . '_unchanged';
562
          $var_info[$name]['rules assertion'] = $var_info[$parts[0]]['rules assertion'];
563
          $var_info[$name]['property info alter'] = array('RulesData', 'applyMetadataAssertions');
564

    
565
          if (isset($var_info[$parts[0]]['bundle']) && !isset($var_info[$name]['bundle'])) {
566
            $var_info[$name]['bundle'] = $var_info[$parts[0]]['bundle'];
567
          }
568
        }
569
      }
570
    }
571
    return $var_info;
572
  }
573

    
574
  /**
575
   * Property info alter callback for the entity metadata wrapper.
576
   *
577
   * Used for applying the rules metadata assertions.
578
   *
579
   * @see RulesData::addMetadataAssertions()
580
   */
581
  public static function applyMetadataAssertions(EntityMetadataWrapper $wrapper, $property_info) {
582
    $info = $wrapper->info();
583

    
584
    if (!empty($info['rules assertion'])) {
585
      $assertion = $info['rules assertion'];
586

    
587
      // In case there are list-wrappers pass through the assertions of the item
588
      // but make sure we only apply the assertions for the list items for
589
      // which the conditions are executed.
590
      if (isset($info['parent']) && $info['parent'] instanceof EntityListWrapper) {
591
        $assertion = isset($assertion[$info['name']]) ? $assertion[$info['name']] : array();
592
      }
593

    
594
      // Support specifying multiple bundles, whereas the added properties are
595
      // the intersection of the bundle properties.
596
      if (isset($assertion['#info']['bundle'])) {
597
        $bundles = (array) $assertion['#info']['bundle'];
598
        foreach ($bundles as $bundle) {
599
          $properties[] = isset($property_info['bundles'][$bundle]['properties']) ? $property_info['bundles'][$bundle]['properties'] : array();
600
        }
601
        // Add the intersection.
602
        $property_info['properties'] += count($properties) > 1 ? call_user_func_array('array_intersect_key', $properties) : reset($properties);
603
      }
604
      // Support adding directly asserted property info.
605
      if (isset($assertion['#info']['property info'])) {
606
        $property_info['properties'] += $assertion['#info']['property info'];
607
      }
608

    
609
      // Pass through any rules assertion of properties to their info, so any
610
      // derived wrappers apply it.
611
      foreach (element_children($assertion) as $key) {
612
        $property_info['properties'][$key]['rules assertion'] = $assertion[$key];
613
        $property_info['properties'][$key]['property info alter'] = array('RulesData', 'applyMetadataAssertions');
614

    
615
        // Apply any 'type' and 'bundle' assertion directly to the property
616
        // info.
617
        if (isset($assertion[$key]['#info']['type'])) {
618
          $type = $assertion[$key]['#info']['type'];
619
          // Support asserting a type in case of generic entity references only.
620
          if ($property_info['properties'][$key]['type'] == 'entity' && entity_get_info($type)) {
621
            $property_info['properties'][$key]['type'] = $type;
622
          }
623
        }
624
        if (isset($assertion[$key]['#info']['bundle'])) {
625
          $bundle = (array) $assertion[$key]['#info']['bundle'];
626
          // Add any single bundle directly to the variable info, so the
627
          // property fits as argument for parameters requiring the bundle.
628
          if (count($bundle) == 1) {
629
            $property_info['properties'][$key]['bundle'] = reset($bundle);
630
          }
631
        }
632
      }
633
    }
634
    return $property_info;
635
  }
636

    
637
  /**
638
   * Property info alter callback for the entity metadata wrapper.
639
   *
640
   * Used to inject metadata for the 'site' variable. In contrast to doing this
641
   * via hook_rules_data_info() this callback makes use of the already existing
642
   * property info cache for site information of entity metadata.
643
   *
644
   * @see RulesPlugin::availableVariables()
645
   */
646
  public static function addSiteMetadata(EntityMetadataWrapper $wrapper, $property_info) {
647
    $site_info = entity_get_property_info('site');
648
    $property_info['properties'] += $site_info['properties'];
649
    // Also invoke the usual callback for altering metadata, in case actions
650
    // have specified further metadata.
651
    return RulesData::applyMetadataAssertions($wrapper, $property_info);
652
  }
653

    
654
}
655

    
656
/**
657
 * A wrapper class similar to the EntityDrupalWrapper, but for non-entities.
658
 *
659
 * This class is intended to serve as base for a custom wrapper classes of
660
 * identifiable data types, which are non-entities. By extending this class only
661
 * the extractIdentifier() and load() methods have to be defined.
662
 * In order to make the data type savable implement the
663
 * RulesDataWrapperSavableInterface.
664
 *
665
 * That way it is possible for non-entity data types to be work with Rules, i.e.
666
 * one can implement a 'ui class' with a direct input form returning the
667
 * identifier of the data. However, instead of that it is suggested to implement
668
 * an entity type, such that the same is achieved via general API functions like
669
 * entity_load().
670
 */
671
abstract class RulesIdentifiableDataWrapper extends EntityStructureWrapper {
672

    
673
  /**
674
   * Contains the id.
675
   */
676
  protected $id = FALSE;
677

    
678
  /**
679
   * Construct a new wrapper object.
680
   *
681
   * @param $type
682
   *   The type of the passed data.
683
   * @param $data
684
   *   Optional. The data to wrap or its identifier.
685
   * @param array $info
686
   *   Optional. Used internally to pass info about properties down the tree.
687
   */
688
  public function __construct($type, $data = NULL, $info = array()) {
689
    parent::__construct($type, $data, $info);
690
    $this->setData($data);
691
  }
692

    
693
  /**
694
   * Sets the data internally accepting both the data id and object.
695
   */
696
  protected function setData($data) {
697
    if (isset($data) && $data !== FALSE && !is_object($data)) {
698
      $this->id = $data;
699
      $this->data = FALSE;
700
    }
701
    elseif (is_object($data)) {
702
      // We got the data object passed.
703
      $this->data = $data;
704
      $id = $this->extractIdentifier($data);
705
      $this->id = isset($id) ? $id : FALSE;
706
    }
707
  }
708

    
709
  /**
710
   * Returns the identifier of the wrapped data.
711
   */
712
  public function getIdentifier() {
713
    return $this->dataAvailable() && $this->value() ? $this->id : NULL;
714
  }
715

    
716
  /**
717
   * Overridden.
718
   */
719
  public function value(array $options = array()) {
720
    $this->setData(parent::value());
721
    if (!$this->data && !empty($this->id)) {
722
      // Lazy load the data if necessary.
723
      $this->data = $this->load($this->id);
724
      if (!$this->data) {
725
        throw new EntityMetadataWrapperException('Unable to load the ' . check_plain($this->type) . ' with the id ' . check_plain($this->id) . '.');
726
      }
727
    }
728
    return $this->data;
729
  }
730

    
731
  /**
732
   * Overridden to support setting the data by either the object or the id.
733
   */
734
  public function set($value) {
735
    if (!$this->validate($value)) {
736
      throw new EntityMetadataWrapperException('Invalid data value given. Be sure it matches the required data type and format.');
737
    }
738
    // As custom wrapper classes can only appear for Rules variables, but not
739
    // as properties we don't have to care about updating the parent.
740
    $this->clear();
741
    $this->setData($value);
742
    return $this;
743
  }
744

    
745
  /**
746
   * Overridden.
747
   */
748
  public function clear() {
749
    $this->id = NULL;
750
    parent::clear();
751
  }
752

    
753
  /**
754
   * Prepare for serialization.
755
   */
756
  public function __sleep() {
757
    $vars = parent::__sleep();
758
    // Don't serialize the loaded data, except for the case the data is not
759
    // saved yet.
760
    if (!empty($this->id)) {
761
      unset($vars['data']);
762
    }
763
    return $vars;
764
  }
765

    
766
  /**
767
   * Prepare for unserialization.
768
   */
769
  public function __wakeup() {
770
    if ($this->id !== FALSE) {
771
      // Make sure data is set, so the data will be loaded when needed.
772
      $this->data = FALSE;
773
    }
774
  }
775

    
776
  /**
777
   * Extract the identifier of the given data object.
778
   *
779
   * @return
780
   *   The extracted identifier.
781
   */
782
  abstract protected function extractIdentifier($data);
783

    
784
  /**
785
   * Load a data object given an identifier.
786
   *
787
   * @return
788
   *   The loaded data object, or FALSE if loading failed.
789
   */
790
  abstract protected function load($id);
791

    
792
}
793

    
794
/**
795
 * Used to declare custom wrapper classes as savable.
796
 */
797
interface RulesDataWrapperSavableInterface {
798

    
799
  /**
800
   * Save the currently wrapped data.
801
   */
802
  public function save();
803

    
804
}