1: <?php
2: /**
3: * An abstraction that defines persistent objects associated with a service
4: *
5: * @copyright 2012-2013 Rackspace Hosting, Inc.
6: * See COPYING for licensing information
7: *
8: * @package phpOpenCloud
9: * @version 1.0
10: * @author Glen Campbell <glen.campbell@rackspace.com>
11: * @author Jamie Hannaford <jamie.hannaford@rackspace.com>
12: */
13:
14: namespace OpenCloud\Common;
15:
16: /**
17: * Represents an object that can be retrieved, created, updated and deleted.
18: *
19: * This class abstracts much of the common functionality between:
20: *
21: * * Nova servers;
22: * * Swift containers and objects;
23: * * DBAAS instances;
24: * * Cinder volumes;
25: * * and various other objects that:
26: * * have a URL;
27: * * can be created, updated, deleted, or retrieved;
28: * * use a standard JSON format with a top-level element followed by
29: * a child object with attributes.
30: *
31: * In general, you can create a persistent object class by subclassing this
32: * class and defining some protected, static variables:
33: *
34: * * $url_resource - the sub-resource value in the URL of the parent. For
35: * example, if the parent URL is `http://something/parent`, then setting this
36: * value to "another" would result in a URL for the persistent object of
37: * `http://something/parent/another`.
38: *
39: * * $json_name - the top-level JSON object name. For example, if the
40: * persistent object is represented by `{"foo": {"attr":value, ...}}`, then
41: * set $json_name to "foo".
42: *
43: * * $json_collection_name - optional; this value is the name of a collection
44: * of the persistent objects. If not provided, it defaults to `json_name`
45: * with an appended "s" (e.g., if `json_name` is "foo", then
46: * `json_collection_name` would be "foos"). Set this value if the collection
47: * name doesn't follow this pattern.
48: *
49: * * $json_collection_element - the common pattern for a collection is:
50: * `{"collection": [{"attr":"value",...}, {"attr":"value",...}, ...]}`
51: * That is, each element of the array is a \stdClass object containing the
52: * object's attributes. In rare instances, the objects in the array
53: * are named, and `json_collection_element` contains the name of the
54: * collection objects. For example, in this JSON response:
55: * `{"allowedDomain":[{"allowedDomain":{"name":"foo"}}]}`,
56: * `json_collection_element` would be set to "allowedDomain".
57: *
58: * The PersistentObject class supports the standard CRUD methods; if these are
59: * not needed (i.e. not supported by the service), the subclass should redefine
60: * these to call the `noCreate`, `noUpdate`, or `noDelete` methods, which will
61: * trigger an appropriate exception. For example, if an object cannot be created:
62: *
63: * function create($params = array())
64: * {
65: * $this->noCreate();
66: * }
67: */
68: abstract class PersistentObject extends Base
69: {
70:
71: private $service;
72:
73: private $parent;
74:
75: protected $id;
76:
77: /**
78: * Retrieves the instance from persistent storage
79: *
80: * @param mixed $service The service object for this resource
81: * @param mixed $info The ID or array/object of data
82: */
83: public function __construct($service = null, $info = null)
84: {
85: if ($service instanceof Service) {
86: $this->setService($service);
87: }
88:
89: if (property_exists($this, 'metadata')) {
90: $this->metadata = new Metadata;
91: }
92:
93: $this->populate($info);
94: }
95:
96: /**
97: * Validates properties that have a namespace: prefix
98: *
99: * If the property prefix: appears in the list of supported extension
100: * namespaces, then the property is applied to the object. Otherwise,
101: * an exception is thrown.
102: *
103: * @param string $name the name of the property
104: * @param mixed $value the property's value
105: * @return void
106: * @throws AttributeError
107: */
108: public function __set($name, $value)
109: {
110: $this->setProperty($name, $value, $this->getService()->namespaces());
111: }
112:
113: /**
114: * Sets the service associated with this resource object.
115: *
116: * @param \OpenCloud\Common\Service $service
117: */
118: public function setService(Service $service)
119: {
120: $this->service = $service;
121: return $this;
122: }
123:
124: /**
125: * Returns the service object for this resource; required for making
126: * requests, etc. because it has direct access to the Connection.
127: *
128: * @return \OpenCloud\Common\Service
129: */
130: public function getService()
131: {
132: if (null === $this->service) {
133: throw new Exceptions\ServiceValueError(
134: 'No service defined'
135: );
136: }
137: return $this->service;
138: }
139:
140: /**
141: * Legacy shortcut to getService
142: *
143: * @return \OpenCloud\Common\Service
144: */
145: public function service()
146: {
147: return $this->getService();
148: }
149:
150: /**
151: * Set the parent object for this resource.
152: *
153: * @param \OpenCloud\Common\PersistentObject $parent
154: */
155: public function setParent(PersistentObject $parent)
156: {
157: $this->parent = $parent;
158: return $this;
159: }
160:
161: /**
162: * Returns the parent.
163: *
164: * @return \OpenCloud\Common\PersistentObject
165: */
166: public function getParent()
167: {
168: if (null === $this->parent) {
169: $this->parent = $this->getService();
170: }
171: return $this->parent;
172: }
173:
174: /**
175: * Legacy shortcut to getParent
176: *
177: * @return \OpenCloud\Common\PersistentObject
178: */
179: public function parent()
180: {
181: return $this->getParent();
182: }
183:
184:
185:
186:
187: /**
188: * API OPERATIONS (CRUD & CUSTOM)
189: */
190:
191: /**
192: * Creates a new object
193: *
194: * @api
195: * @param array $params array of values to set when creating the object
196: * @return HttpResponse
197: * @throws VolumeCreateError if HTTP status is not Success
198: */
199: public function create($params = array())
200: {
201: // set parameters
202: if (!empty($params)) {
203: $this->populate($params, false);
204: }
205:
206: // debug
207: $this->getLogger()->info('{class}::Create({name})', array(
208: 'class' => get_class($this),
209: 'name' => $this->Name()
210: ));
211:
212: // construct the JSON
213: $object = $this->createJson();
214: $json = json_encode($object);
215: $this->checkJsonError();
216:
217: $this->getLogger()->info('{class}::Create JSON [{json}]', array(
218: 'class' => get_class($this),
219: 'json' => $json
220: ));
221:
222: // send the request
223: $response = $this->getService()->request(
224: $this->createUrl(),
225: 'POST',
226: array('Content-Type' => 'application/json'),
227: $json
228: );
229:
230: // check the return code
231: // @codeCoverageIgnoreStart
232: if ($response->httpStatus() > 204) {
233: throw new Exceptions\CreateError(sprintf(
234: Lang::translate('Error creating [%s] [%s], status [%d] response [%s]'),
235: get_class($this),
236: $this->Name(),
237: $response->HttpStatus(),
238: $response->HttpBody()
239: ));
240: }
241:
242: if ($response->HttpStatus() == "201" && ($location = $response->Header('Location'))) {
243: // follow Location header
244: $this->refresh(null, $location);
245: } else {
246: // set values from response
247: $object = json_decode($response->httpBody());
248:
249: if (!$this->checkJsonError()) {
250: $top = $this->jsonName();
251: if (isset($object->$top)) {
252: $this->populate($object->$top);
253: }
254: }
255: }
256: // @codeCoverageIgnoreEnd
257:
258: return $response;
259: }
260:
261: /**
262: * Updates an existing object
263: *
264: * @api
265: * @param array $params array of values to set when updating the object
266: * @return HttpResponse
267: * @throws VolumeCreateError if HTTP status is not Success
268: */
269: public function update($params = array())
270: {
271: // set parameters
272: if (!empty($params)) {
273: $this->populate($params);
274: }
275:
276: // debug
277: $this->getLogger()->info('{class}::Update({name})', array(
278: 'class' => get_class($this),
279: 'name' => $this->Name()
280: ));
281:
282: // construct the JSON
283: $obj = $this->updateJson($params);
284: $json = json_encode($obj);
285:
286: $this->checkJsonError();
287:
288: $this->getLogger()->info('{class}::Update JSON [{json}]', array(
289: 'class' => get_class($this),
290: 'json' => $json
291: ));
292:
293: // send the request
294: $response = $this->getService()->Request(
295: $this->url(),
296: 'PUT',
297: array(),
298: $json
299: );
300:
301: // check the return code
302: // @codeCoverageIgnoreStart
303: if ($response->HttpStatus() > 204) {
304: throw new Exceptions\UpdateError(sprintf(
305: Lang::translate('Error updating [%s] with [%s], status [%d] response [%s]'),
306: get_class($this),
307: $json,
308: $response->HttpStatus(),
309: $response->HttpBody()
310: ));
311: }
312: // @codeCoverageIgnoreEnd
313:
314: return $response;
315: }
316:
317: /**
318: * Deletes an object
319: *
320: * @api
321: * @return HttpResponse
322: * @throws DeleteError if HTTP status is not Success
323: */
324: public function delete()
325: {
326: $this->getLogger()->info('{class}::Delete()', array('class' => get_class($this)));
327:
328: // send the request
329: $response = $this->getService()->request($this->url(), 'DELETE');
330:
331: // check the return code
332: // @codeCoverageIgnoreStart
333: if ($response->HttpStatus() > 204) {
334: throw new Exceptions\DeleteError(sprintf(
335: Lang::translate('Error deleting [%s] [%s], status [%d] response [%s]'),
336: get_class(),
337: $this->Name(),
338: $response->HttpStatus(),
339: $response->HttpBody()
340: ));
341: }
342: // @codeCoverageIgnoreEnd
343:
344: return $response;
345: }
346:
347: /**
348: * Returns an object for the Create() method JSON
349: * Must be overridden in a child class.
350: *
351: * @throws CreateError if not overridden
352: */
353: protected function createJson()
354: {
355: throw new Exceptions\CreateError(sprintf(
356: Lang::translate('[%s] CreateJson() must be overridden'),
357: get_class($this)
358: ));
359: }
360:
361: /**
362: * Returns an object for the Update() method JSON
363: * Must be overridden in a child class.
364: *
365: * @throws UpdateError if not overridden
366: */
367: protected function updateJson($params = array())
368: {
369: throw new Exceptions\UpdateError(sprintf(
370: Lang::translate('[%s] UpdateJson() must be overridden'),
371: get_class($this)
372: ));
373: }
374:
375: /**
376: * throws a CreateError for subclasses that don't support Create
377: *
378: * @throws CreateError
379: */
380: protected function noCreate()
381: {
382: throw new Exceptions\CreateError(sprintf(
383: Lang::translate('[%s] does not support Create()'),
384: get_class()
385: ));
386: }
387:
388: /**
389: * throws a DeleteError for subclasses that don't support Delete
390: *
391: * @throws DeleteError
392: */
393: protected function noDelete()
394: {
395: throw new Exceptions\DeleteError(sprintf(
396: Lang::translate('[%s] does not support Delete()'),
397: get_class()
398: ));
399: }
400:
401: /**
402: * throws a UpdateError for subclasses that don't support Update
403: *
404: * @throws UpdateError
405: */
406: protected function noUpdate()
407: {
408: throw new Exceptions\UpdateError(sprintf(
409: Lang::translate('[%s] does not support Update()'),
410: get_class()
411: ));
412: }
413:
414: /**
415: * Returns the default URL of the object
416: *
417: * This may have to be overridden in subclasses.
418: *
419: * @param string $subresource optional sub-resource string
420: * @param array $qstr optional k/v pairs for query strings
421: * @return string
422: * @throws UrlError if URL is not defined
423: */
424: public function url($subresource = null, $queryString = array())
425: {
426: // find the primary key attribute name
427: $primaryKey = $this->primaryKeyField();
428:
429: // first, see if we have a [self] link
430: $url = $this->findLink('self');
431:
432: /**
433: * Next, check to see if we have an ID
434: * Note that we use Parent() instead of Service(), since the parent
435: * object might not be a service.
436: */
437: if (!$url && $this->$primaryKey) {
438: $url = Lang::noslash($this->getParent()->url($this->resourceName())) . '/' . $this->$primaryKey;
439: }
440:
441: // add the subresource
442: if ($url) {
443: $url .= $subresource ? "/$subresource" : '';
444: if (count($queryString)) {
445: $url .= '?' . $this->makeQueryString($queryString);
446: }
447: return $url;
448: }
449:
450: // otherwise, we don't have a URL yet
451: throw new Exceptions\UrlError(sprintf(
452: Lang::translate('%s does not have a URL yet'),
453: get_class($this)
454: ));
455: }
456:
457: /**
458: * Waits for the server/instance status to change
459: *
460: * This function repeatedly polls the system for a change in server
461: * status. Once the status reaches the `$terminal` value (or 'ERROR'),
462: * then the function returns.
463: *
464: * The polling interval is set by the constant RAXSDK_POLL_INTERVAL.
465: *
466: * The function will automatically terminate after RAXSDK_SERVER_MAXTIMEOUT
467: * seconds elapse.
468: *
469: * @api
470: * @param string $terminal the terminal state to wait for
471: * @param integer $timeout the max time (in seconds) to wait
472: * @param callable $callback a callback function that is invoked with
473: * each repetition of the polling sequence. This can be used, for
474: * example, to update a status display or to permit other operations
475: * to continue
476: * @return void
477: */
478: public function waitFor(
479: $terminal = 'ACTIVE',
480: $timeout = RAXSDK_SERVER_MAXTIMEOUT,
481: $callback = NULL,
482: $sleep = RAXSDK_POLL_INTERVAL
483: ) {
484: // find the primary key field
485: $primaryKey = $this->PrimaryKeyField();
486:
487: // save stats
488: $startTime = time();
489:
490: $states = array('ERROR', $terminal);
491:
492: while (true) {
493:
494: $this->refresh($this->$primaryKey);
495:
496: if ($callback) {
497: call_user_func($callback, $this);
498: }
499:
500: if (in_array($this->status(), $states) || (time() - $startTime) > $timeout) {
501: return;
502: }
503: // @codeCoverageIgnoreStart
504: sleep($sleep);
505: }
506: }
507: // @codeCoverageIgnoreEnd
508:
509: /**
510: * Refreshes the object from the origin (useful when the server is
511: * changing states)
512: *
513: * @return void
514: * @throws IdRequiredError
515: */
516: public function refresh($id = null, $url = null)
517: {
518: $primaryKey = $this->PrimaryKeyField();
519:
520: if (!$url) {
521: if ($id === null) {
522: $id = $this->$primaryKey;
523: }
524:
525: if (!$id) {
526: throw new Exceptions\IdRequiredError(sprintf(
527: Lang::translate('%s has no ID; cannot be refreshed'),
528: get_class())
529: );
530: }
531:
532: // retrieve it
533: $this->getLogger()->info(Lang::translate('{class} id [{id}]'), array(
534: 'class' => get_class($this),
535: 'id' => $id
536: ));
537:
538: $this->$primaryKey = $id;
539: $url = $this->url();
540: }
541:
542: // reset status, if available
543: if (property_exists($this, 'status')) {
544: $this->status = null;
545: }
546:
547: // perform a GET on the URL
548: $response = $this->getService()->Request($url);
549:
550: // check status codes
551: // @codeCoverageIgnoreStart
552: if ($response->HttpStatus() == 404) {
553: throw new Exceptions\InstanceNotFound(
554: sprintf(Lang::translate('%s [%s] not found [%s]'),
555: get_class($this),
556: $this->$primaryKey,
557: $url
558: ));
559: }
560:
561: if ($response->HttpStatus() >= 300) {
562: throw new Exceptions\UnknownError(
563: sprintf(Lang::translate('Unexpected %s error [%d] [%s]'),
564: get_class($this),
565: $response->HttpStatus(),
566: $response->HttpBody()
567: ));
568: }
569:
570: // check for empty response
571: if (!$response->HttpBody()) {
572: throw new Exceptions\EmptyResponseError(
573: sprintf(Lang::translate('%s::Refresh() unexpected empty response, URL [%s]'),
574: get_class($this),
575: $url
576: ));
577: }
578:
579: // we're ok, reload the response
580: if ($json = $response->HttpBody()) {
581:
582: $this->getLogger()->info('refresh() JSON [{json}]', array('json' => $json));
583:
584: $response = json_decode($json);
585:
586: if ($this->CheckJsonError()) {
587: throw new Exceptions\ServerJsonError(sprintf(
588: Lang::translate('JSON parse error on %s refresh'),
589: get_class($this)
590: ));
591: }
592:
593: $top = $this->JsonName();
594:
595: if ($top && isset($response->$top)) {
596: $content = $response->$top;
597: } else {
598: $content = $response;
599: }
600:
601: $this->populate($content);
602:
603: }
604: // @codeCoverageIgnoreEnd
605: }
606:
607:
608: /**
609: * OBJECT INFORMATION
610: */
611:
612: /**
613: * Returns the displayable name of the object
614: *
615: * Can be overridden by child objects; *must* be overridden by child
616: * objects if the object does not have a `name` attribute defined.
617: *
618: * @api
619: * @return string
620: * @throws NameError if attribute 'name' is not defined
621: */
622: public function name()
623: {
624: if (property_exists($this, 'name')) {
625: return $this->name;
626: } else {
627: throw new Exceptions\NameError(sprintf(
628: Lang::translate('Name attribute does not exist for [%s]'),
629: get_class($this)
630: ));
631: }
632: }
633:
634: /**
635: * Sends the json string to the /action resource
636: *
637: * This is used for many purposes, such as rebooting the server,
638: * setting the root password, creating images, etc.
639: * Since it can only be used on a live server, it checks for a valid ID.
640: *
641: * @param $object - this will be encoded as json, and we handle all the JSON
642: * error-checking in one place
643: * @throws ServerIdError if server ID is not defined
644: * @throws ServerActionError on other errors
645: * @returns boolean; TRUE if successful, FALSE otherwise
646: */
647: protected function action($object)
648: {
649: $primaryKey = $this->primaryKeyField();
650:
651: if (!$this->$primaryKey) {
652: throw new Exceptions\IdRequiredError(sprintf(
653: Lang::translate('%s is not defined'),
654: get_class($this)
655: ));
656: }
657:
658: if (!is_object($object)) {
659: throw new Exceptions\ServerActionError(sprintf(
660: Lang::translate('%s::Action() requires an object as its parameter'),
661: get_class($this)
662: ));
663: }
664:
665: // convert the object to json
666: $json = json_encode($object);
667: $this->getLogger()->info('JSON [{string}]', array('json' => $json));
668:
669: $this->checkJsonError();
670:
671: // debug - save the request
672: $this->getLogger()->info(Lang::translate('{class}::action [{json}]'), array(
673: 'class' => get_class($this),
674: 'json' => $json
675: ));
676:
677: // get the URL for the POST message
678: $url = $this->url('action');
679:
680: // POST the message
681: $response = $this->getService()->request($url, 'POST', array(), $json);
682:
683: // @codeCoverageIgnoreStart
684: if (!is_object($response)) {
685: throw new Exceptions\HttpError(sprintf(
686: Lang::translate('Invalid response for %s::Action() request'),
687: get_class($this)
688: ));
689: }
690:
691: // check for errors
692: if ($response->HttpStatus() >= 300) {
693: throw new Exceptions\ServerActionError(sprintf(
694: Lang::translate('%s::Action() [%s] failed; response [%s]'),
695: get_class($this),
696: $url,
697: $response->HttpBody()
698: ));
699: }
700: // @codeCoverageIgnoreStart
701:
702: return $response;
703: }
704:
705: /**
706: * Execute a custom resource request.
707: *
708: * @param string $path
709: * @param string $method
710: * @param string|array|object $body
711: * @return boolean
712: * @throws Exceptions\InvalidArgumentError
713: * @throws Exceptions\HttpError
714: * @throws Exceptions\ServerActionError
715: */
716: public function customAction($url, $method = 'GET', $body = null)
717: {
718: if (is_string($body) && (json_decode($body) === null)) {
719: throw new Exceptions\InvalidArgumentError(
720: 'Please provide either a well-formed JSON string, or an object '
721: . 'for JSON serialization'
722: );
723: } else {
724: $body = json_encode($body);
725: }
726:
727: // POST the message
728: $response = $this->service()->request($url, $method, array(), $body);
729:
730: if (!is_object($response)) {
731: throw new Exceptions\HttpError(sprintf(
732: Lang::translate('Invalid response for %s::customAction() request'),
733: get_class($this)
734: ));
735: }
736:
737: // check for errors
738: // @codeCoverageIgnoreStart
739: if ($response->HttpStatus() >= 300) {
740: throw new Exceptions\ServerActionError(sprintf(
741: Lang::translate('%s::customAction() [%s] failed; response [%s]'),
742: get_class($this),
743: $url,
744: $response->HttpBody()
745: ));
746: }
747: // @codeCoverageIgnoreEnd
748:
749: $object = json_decode($response->httpBody());
750:
751: $this->checkJsonError();
752:
753: return $object;
754: }
755:
756: /**
757: * returns the object's status or `N/A` if not available
758: *
759: * @api
760: * @return string
761: */
762: public function status()
763: {
764: return (isset($this->status)) ? $this->status : 'N/A';
765: }
766:
767: /**
768: * returns the object's identifier
769: *
770: * Can be overridden by a child class if the identifier is not in the
771: * `$id` property. Use of this function permits the `$id` attribute to
772: * be protected or private to prevent unauthorized overwriting for
773: * security.
774: *
775: * @api
776: * @return string
777: */
778: public function id()
779: {
780: return $this->id;
781: }
782:
783: /**
784: * checks for `$alias` in extensions and throws an error if not present
785: *
786: * @throws UnsupportedExtensionError
787: */
788: public function checkExtension($alias)
789: {
790: if (!in_array($alias, $this->getService()->namespaces())) {
791: throw new Exceptions\UnsupportedExtensionError(sprintf(
792: Lang::translate('Extension [%s] is not installed'),
793: $alias
794: ));
795: }
796:
797: return true;
798: }
799:
800: /**
801: * returns the region associated with the object
802: *
803: * navigates to the parent service to determine the region.
804: *
805: * @api
806: */
807: public function region()
808: {
809: return $this->getService()->Region();
810: }
811:
812: /**
813: * Since each server can have multiple links, this returns the desired one
814: *
815: * @param string $type - 'self' is most common; use 'bookmark' for
816: * the version-independent one
817: * @return string the URL from the links block
818: */
819: public function findLink($type = 'self')
820: {
821: if (empty($this->links)) {
822: return false;
823: }
824:
825: foreach ($this->links as $link) {
826: if ($link->rel == $type) {
827: return $link->href;
828: }
829: }
830:
831: return false;
832: }
833:
834: /**
835: * returns the URL used for Create
836: *
837: * @return string
838: */
839: protected function createUrl()
840: {
841: return $this->getParent()->Url($this->ResourceName());
842: }
843:
844: /**
845: * Returns the primary key field for the object
846: *
847: * The primary key is usually 'id', but this function is provided so that
848: * (in rare cases where it is not 'id'), it can be overridden.
849: *
850: * @return string
851: */
852: protected function primaryKeyField()
853: {
854: return 'id';
855: }
856:
857: /**
858: * Returns the top-level document identifier for the returned response
859: * JSON document; must be overridden in child classes
860: *
861: * For example, a server document is (JSON) `{"server": ...}` and an
862: * Instance document is `{"instance": ...}` - this function must return
863: * the top level document name (either "server" or "instance", in
864: * these examples).
865: *
866: * @throws DocumentError if not overridden
867: */
868: public static function jsonName()
869: {
870: if (isset(static::$json_name)) {
871: return static::$json_name;
872: }
873:
874: throw new Exceptions\DocumentError(sprintf(
875: Lang::translate('No JSON object defined for class [%s] in JsonName()'),
876: get_class()
877: ));
878: }
879:
880: /**
881: * returns the collection JSON element name
882: *
883: * When an object is returned in a collection, it usually has a top-level
884: * object that is an array holding child objects of the object types.
885: * This static function returns the name of the top-level element. Usually,
886: * that top-level element is simply the JSON name of the resource.'s';
887: * however, it can be overridden by specifying the $json_collection_name
888: * attribute.
889: *
890: * @return string
891: */
892: public static function jsonCollectionName()
893: {
894: if (isset(static::$json_collection_name)) {
895: return static::$json_collection_name;
896: } else {
897: return static::$json_name . 's';
898: }
899: }
900:
901: /**
902: * returns the JSON name for each element in a collection
903: *
904: * Usually, elements in a collection are anonymous; this function, however,
905: * provides for an element level name:
906: *
907: * `{ "collection" : [ { "element" : ... } ] }`
908: *
909: * @return string
910: */
911: public static function jsonCollectionElement()
912: {
913: if (isset(static::$json_collection_element)) {
914: return static::$json_collection_element;
915: }
916: }
917:
918: /**
919: * Returns the resource name for the URL of the object; must be overridden
920: * in child classes
921: *
922: * For example, a server is `/servers/`, a database instance is
923: * `/instances/`. Must be overridden in child classes.
924: *
925: * @throws UrlError
926: */
927: public static function resourceName()
928: {
929: if (isset(static::$url_resource)) {
930: return static::$url_resource;
931: }
932:
933: throw new Exceptions\UrlError(sprintf(
934: Lang::translate('No URL resource defined for class [%s] in ResourceName()'),
935: get_class()
936: ));
937: }
938:
939: }