1<?php
2namespace Elgg;
3
4/**
5 * Events service
6 *
7 * Use elgg()->events
8 */
9class EventsService extends HooksRegistrationService {
10	use Profilable;
11
12	const OPTION_STOPPABLE = 'stoppable';
13
14	/**
15	 * @var HandlersService
16	 */
17	private $handlers;
18
19	/**
20	 * Constructor
21	 *
22	 * @param HandlersService $handlers Handlers
23	 */
24	public function __construct(HandlersService $handlers) {
25		$this->handlers = $handlers;
26	}
27
28	/**
29	 * Get the handlers service in use
30	 *
31	 * @return HandlersService
32	 * @internal
33	 */
34	public function getHandlersService() {
35		return $this->handlers;
36	}
37
38	/**
39	 * {@inheritdoc}
40	 */
41	public function registerHandler($name, $type, $callback, $priority = 500) {
42		if (in_array($type, ['member', 'friend', 'attached'])
43				&& in_array($name, ['create', 'update', 'delete'])) {
44			_elgg_services()->logger->error("'$name, $type' event is no longer triggered. "
45				. "Update your event registration to use '$name, relationship'");
46		}
47
48		return parent::registerHandler($name, $type, $callback, $priority);
49	}
50
51	/**
52	 * Triggers an Elgg event
53	 *
54	 * @param string $event       The event type
55	 * @param string $object_type The object type
56	 * @param mixed  $object      The object involved in the event
57	 * @param array  $options     (internal) options for triggering the event
58	 *
59	 * @see elgg_trigger_event()
60	 * @see elgg_trigger_after_event()
61	 * @see elgg_trigger_before_event()
62	 *
63	 * @return bool
64	 */
65	public function trigger($name, $type, $object = null, array $options = []) {
66		$options = array_merge([
67			self::OPTION_STOPPABLE => true,
68		], $options);
69
70		// check for deprecation
71		$this->checkDeprecation($name, $type, $options);
72
73		// get registered handlers
74		$handlers = $this->getOrderedHandlers($name, $type);
75
76		// This starts as a string, but if a handler type-hints an object we convert it on-demand inside
77		// \Elgg\HandlersService::call and keep it alive during all handler calls. We do this because
78		// creating objects for every triggering is expensive.
79		$event = 'event';
80		/* @var Event|string */
81
82		foreach ($handlers as $handler) {
83			$handler_description = false;
84			if ($this->timer && $type === 'system' && $name !== 'shutdown') {
85				$handler_description = $this->handlers->describeCallable($handler) . "()";
86				$this->timer->begin(["[$name,$type]", $handler_description]);
87			}
88
89			list($success, $return, $event) = $this->handlers->call($handler, $event, [$name, $type, $object]);
90
91			if ($handler_description) {
92				$this->timer->end(["[$name,$type]", $handler_description]);
93			}
94
95			if (!$success) {
96				continue;
97			}
98
99			if (!empty($options[self::OPTION_STOPPABLE]) && ($return === false)) {
100				return false;
101			}
102		}
103
104		return true;
105	}
106
107	/**
108	 * Trigger a "Before event" indicating a process is about to begin.
109	 *
110	 * Like regular events, a handler returning false will cancel the process and false
111	 * will be returned.
112	 *
113	 * To register for a before event, append ":before" to the event name when registering.
114	 *
115	 * @param string $event       The event type. The fired event type will be appended with ":before".
116	 * @param string $object_type The object type
117	 * @param mixed  $object      The object involved in the event
118	 * @param array  $options     (internal) options for triggering the event
119	 *
120	 * @return bool False if any handler returned false, otherwise true
121	 *
122	 * @see EventsService::trigger()
123	 * @see EventsService::triggerAfter()
124	 * @since 2.0.0
125	 */
126	public function triggerBefore($event, $object_type, $object = null, array $options = []) {
127		return $this->trigger("$event:before", $object_type, $object, $options);
128	}
129
130	/**
131	 * Trigger an "After event" indicating a process has finished.
132	 *
133	 * Unlike regular events, all the handlers will be called, their return values ignored.
134	 *
135	 * To register for an after event, append ":after" to the event name when registering.
136	 *
137	 * @param string $event       The event type. The fired event type will be appended with ":after".
138	 * @param string $object_type The object type
139	 * @param mixed  $object      The object involved in the event
140	 * @param array  $options     (internal) options for triggering the event
141	 *
142	 * @return true
143	 *
144	 * @see EventsService::trigger()
145	 * @see EventsService::triggerBefore()
146	 * @since 2.0.0
147	 */
148	public function triggerAfter($event, $object_type, $object = null, array $options = []) {
149		$options[self::OPTION_STOPPABLE] = false;
150
151		return $this->trigger("$event:after", $object_type, $object, $options);
152	}
153
154	/**
155	 * Trigger an sequence of <event>:before, <event>, and <event>:after handlers.
156	 * Allows <event>:before to terminate the sequence by returning false from a handler
157	 * Allows running a callable on successful <event> before <event>:after is triggered
158	 * Returns the result of the callable or bool
159	 *
160	 * @param string   $event       The event type
161	 * @param string   $object_type The object type
162	 * @param mixed    $object      The object involved in the event
163	 * @param callable $callable    Callable to run on successful event, before event:after
164	 * @param array    $options     (internal) options for triggering the event
165	 *
166	 * @return mixed
167	 */
168	public function triggerSequence($event, $object_type, $object = null, callable $callable = null, array $options = []) {
169		if (!$this->triggerBefore($event, $object_type, $object, $options)) {
170			return false;
171		}
172
173		$result = $this->trigger($event, $object_type, $object, $options);
174		if (!$result) {
175			return false;
176		}
177
178		if ($callable) {
179			$result = call_user_func($callable, $object);
180		}
181
182		$this->triggerAfter($event, $object_type, $object, $options);
183
184		return $result;
185	}
186
187	/**
188	 * Trigger an event sequence normally, but send a notice about deprecated use if any handlers are registered.
189	 *
190	 * @param string $event       The event type
191	 * @param string $object_type The object type
192	 * @param mixed  $object      The object involved in the event
193	 * @param string $message     The deprecation message
194	 * @param string $version     Human-readable *release* version: 1.9, 1.10, ...
195	 *
196	 * @return bool
197	 *
198	 * @see EventsService::trigger()
199	 * @see elgg_trigger_deprecated_event()
200	 */
201	public function triggerDeprecated($event, $object_type, $object = null, $message = null, $version = null) {
202		$options = [
203			self::OPTION_DEPRECATION_MESSAGE => $message,
204			self::OPTION_DEPRECATION_VERSION => $version,
205		];
206		return $this->trigger($event, $object_type, $object, $options);
207	}
208
209	/**
210	 * Trigger an event normally, but send a notice about deprecated use if any handlers are registered.
211	 *
212	 * @param string   $event       The event type
213	 * @param string   $object_type The object type
214	 * @param mixed    $object      The object involved in the event
215	 * @param callable $callable    Callable to run on successful event, before event:after
216	 * @param string   $message     The deprecation message
217	 * @param string   $version     Human-readable *release* version: 1.9, 1.10, ...
218	 *
219	 * @return bool
220	 *
221	 * @see EventsService::trigger()
222	 */
223	public function triggerDeprecatedSequence($event, $object_type, $object = null, callable $callable = null, string $message = null, string $version = null) {
224		$options = [
225			self::OPTION_DEPRECATION_MESSAGE => $message,
226			self::OPTION_DEPRECATION_VERSION => $version,
227		];
228		return $this->triggerSequence($event, $object_type, $object, $callable, $options);
229	}
230}
231