1<?php 2 3 4namespace ComboStrap; 5 6 7use action_plugin_combo_metadescription; 8use ModificationDate; 9use Slug; 10 11abstract class Metadata 12{ 13 const CANONICAL = "metadata"; 14 15 public const NOT_MODIFIABLE_METAS = [ 16 "date", 17 "user", 18 "last_change", 19 "creator", 20 "contributor" 21 ]; 22 23 24 25 /** 26 * @var bool 27 */ 28 protected $wasBuild = false; 29 30 /** 31 * The metadata is for this resource 32 * @var ResourceCombo $resource 33 */ 34 private $resource; 35 36 /** 37 * @var MetadataStore 38 */ 39 private $readStore; 40 /** 41 * @var Metadata|null 42 */ 43 private $parent; 44 /** 45 * @var MetadataStore 46 */ 47 private $writeStore; 48 /** 49 * @var Metadata 50 */ 51 private $uidObject; 52 /** 53 * @var Metadata[] 54 */ 55 private $childrenObject; 56 57 /** 58 * The metadata may be just not stored 59 * The page is just the scope 60 */ 61 public function __construct(Metadata $parent = null) 62 { 63 $this->parent = $parent; 64 } 65 66 /** 67 * @param $class 68 * @param Metadata|null $parent 69 * @return Metadata 70 * @throws ExceptionCombo 71 */ 72 public static function toMetadataObject($class, Metadata $parent = null): Metadata 73 { 74 if ($class === null) { 75 throw new ExceptionCombo("The string class is empty"); 76 } 77 if (!is_subclass_of($class, Metadata::class)) { 78 throw new ExceptionCombo("The class ($class) is not a metadata class"); 79 } 80 return new $class($parent); 81 } 82 83 84 public function getParent(): ?Metadata 85 { 86 return $this->parent; 87 } 88 89 /** 90 * The class string of the child/columns metadata 91 * @return null|string[]; 92 */ 93 public function getChildrenClass(): ?array 94 { 95 return null; 96 } 97 98 public static function getForName(string $name): ?Metadata 99 { 100 101 $name = strtolower(trim($name)); 102 /** 103 * TODO: this array could be build automatically by creating an object for each metadata 104 */ 105 switch ($name) { 106 case Canonical::getName(): 107 return new Canonical(); 108 case PageType::getName(): 109 return new PageType(); 110 case PageH1::getName(): 111 return new PageH1(); 112 case Aliases::getName(): 113 case AliasPath::getName(): 114 case AliasType::getName(): 115 return new Aliases(); 116 case PageImages::getName(): 117 case PageImages::OLD_PROPERTY_NAME: 118 case PageImages::getPersistentName(): 119 case PageImagePath::getName(): 120 case PageImageUsage::getName(): 121 return new PageImages(); 122 case Region::OLD_REGION_PROPERTY: 123 case Region::getName(): 124 return new Region(); 125 case Lang::PROPERTY_NAME: 126 return new Lang(); 127 case PageTitle::TITLE: 128 return new PageTitle(); 129 case PagePublicationDate::OLD_META_KEY: 130 case PagePublicationDate::PROPERTY_NAME: 131 return new PagePublicationDate(); 132 case ResourceName::PROPERTY_NAME: 133 return new ResourceName(); 134 case LdJson::OLD_ORGANIZATION_PROPERTY: 135 case LdJson::PROPERTY_NAME: 136 return new LdJson(); 137 case PageLayout::PROPERTY_NAME: 138 return new PageLayout(); 139 case StartDate::PROPERTY_NAME: 140 return new StartDate(); 141 case EndDate::PROPERTY_NAME: 142 return new EndDate(); 143 case PageDescription::DESCRIPTION_PROPERTY: 144 return new PageDescription(); 145 case Slug::PROPERTY_NAME: 146 return new Slug(); 147 case PageKeywords::PROPERTY_NAME: 148 return new PageKeywords(); 149 case CacheExpirationFrequency::PROPERTY_NAME: 150 return new CacheExpirationFrequency(); 151 case QualityDynamicMonitoringOverwrite::PROPERTY_NAME: 152 return new QualityDynamicMonitoringOverwrite(); 153 case LowQualityPageOverwrite::PROPERTY_NAME: 154 return new LowQualityPageOverwrite(); 155 case PageId::PROPERTY_NAME: 156 return new PageId(); 157 case PagePath::PROPERTY_NAME: 158 return new PagePath(); 159 case PageCreationDate::PROPERTY_NAME: 160 return new PageCreationDate(); 161 case ModificationDate::PROPERTY_NAME: 162 return new ModificationDate(); 163 case DokuwikiId::DOKUWIKI_ID_ATTRIBUTE: 164 return new DokuwikiId(); 165 case PageUrlPath::PROPERTY_NAME: 166 return new PageUrlPath(); 167 case Locale::PROPERTY_NAME: 168 return new Locale(); 169 case CacheExpirationDate::PROPERTY_NAME: 170 return new CacheExpirationDate(); 171 case \ReplicationDate::getName(): 172 return new \ReplicationDate(); 173 case DisqusIdentifier::PROPERTY_NAME: 174 return new DisqusIdentifier(); 175 default: 176 $msg = "The metadata ($name) can't be retrieved in the list of metadata. It should be defined"; 177 LogUtility::msg($msg, LogUtility::LVL_MSG_INFO, self::CANONICAL); 178 } 179 return null; 180 181 } 182 183 public function toStoreValueOrDefault() 184 { 185 $value = $this->toStoreValue(); 186 if ($value !== null) { 187 return $value; 188 } 189 return $this->toStoreDefaultValue(); 190 } 191 192 public function getChildrenObject() 193 { 194 if ($this->getChildrenClass() === null) { 195 return null; 196 } 197 if ($this->childrenObject !== null) { 198 return $this->childrenObject; 199 } 200 foreach ($this->getChildrenClass() as $childrenClass) { 201 try { 202 $this->childrenObject[] = Metadata::toMetadataObject($childrenClass) 203 ->setResource($this->getResource()); 204 } catch (ExceptionCombo $e) { 205 LogUtility::msg("Unable to build the metadata children object: " . $e->getMessage()); 206 } 207 } 208 return $this->childrenObject; 209 210 } 211 212 213 /** 214 * @param $store 215 * @return $this 216 */ 217 public function setReadStore($store): Metadata 218 { 219 if ($this->readStore !== null) { 220 LogUtility::msg("The read store was already set."); 221 } 222 if (is_string($store) && !is_subclass_of($store, MetadataStore::class)) { 223 throw new ExceptionComboRuntime("The store class ($store) is not a metadata store class"); 224 } 225 $this->readStore = $store; 226 return $this; 227 } 228 229 /** 230 * @param MetadataStore|string $store 231 * @return $this 232 */ 233 public function setWriteStore($store): Metadata 234 { 235 $this->writeStore = $store; 236 return $this; 237 } 238 239 /** 240 * @param mixed $value 241 * @return Metadata 242 */ 243 public abstract function setValue($value): Metadata; 244 245 /** 246 * @return bool 247 * used in the {@link Metadata::buildCheck()} function 248 * If the value is null, the {@link Metadata::buildFromReadStore()} will be performed 249 * otherwise, it will not 250 */ 251 public abstract function valueIsNotNull(): bool; 252 253 /** 254 * If the {@link Metadata::getValue()} is null and if the object was not already build 255 * this function will call the function {@link Metadata::buildFromReadStore()} 256 */ 257 protected function buildCheck() 258 { 259 if (!$this->wasBuild && !$this->valueIsNotNull()) { 260 $this->wasBuild = true; 261 $this->buildFromReadStore(); 262 } 263 } 264 265 /** 266 * Return the store for this metadata 267 * By default, this is the {@link ResourceCombo::getReadStoreOrDefault() default resource metadata store} 268 * 269 * (ie a memory variable or a database) 270 * @return MetadataStore|null 271 */ 272 public function getReadStore(): ?MetadataStore 273 { 274 if ($this->readStore === null) { 275 return $this->getResource()->getReadStoreOrDefault(); 276 } 277 if (!$this->readStore instanceof MetadataStore) { 278 $this->readStore = MetadataStoreAbs::toMetadataStore($this->readStore, $this->getResource()); 279 } 280 return $this->readStore; 281 } 282 283 public function getTab(): ?string 284 { 285 return $this->getName(); 286 } 287 288 /** 289 * This function sends the object value to the {@link Metadata::getReadStore() store} 290 * 291 * This function should be used at the end of each setter/adder function 292 * 293 * @throws ExceptionCombo 294 * 295 * To persist or commit on disk, you use the {@link MetadataStore::persist()} 296 * Because the metadata is stored by resource, the persist function is 297 * also made available on the resource level 298 * 299 */ 300 public function sendToWriteStore(): Metadata 301 { 302 $this->getWriteStore()->set($this); 303 return $this; 304 } 305 306 /** 307 * @return string - the name to lookup the value 308 * This is the column name in a database or the property name in a key value store 309 * It should be unique over all metadata 310 */ 311 public function __toString() 312 { 313 return $this->getName(); 314 } 315 316 /** @noinspection PhpMissingReturnTypeInspection */ 317 public function buildFromReadStore() 318 { 319 $this->wasBuild = true; 320 $metadataStore = $this->getReadStore(); 321 if ($metadataStore === null) { 322 LogUtility::msg("The metadata store is unknown. You need to define a resource or a store to build from it"); 323 return $this; 324 } 325 $value = $metadataStore->get($this); 326 $this->buildFromStoreValue($value); 327 return $this; 328 } 329 330 331 /** 332 * @return string - the data type 333 * used: 334 * * to store the data in the database 335 * * to select the type of input in a HTML form 336 */ 337 public abstract function getDataType(): string; 338 339 /** 340 * @return string - the description (used in tooltip) 341 */ 342 public abstract function getDescription(): string; 343 344 /** 345 * @return string - the label used in a form or log 346 */ 347 public abstract function getLabel(): string; 348 349 public function getCanonical(): string 350 { 351 if ($this->parent !== null) { 352 $canonical = $this->parent->getCanonical(); 353 if ($canonical !== null) { 354 return $canonical; 355 } 356 } 357 /** 358 * The canonical to page metadata 359 */ 360 return self::CANONICAL; 361 } 362 363 /** 364 * @return ResourceCombo - The resource 365 */ 366 public function getResource(): ?ResourceCombo 367 { 368 if ($this->resource !== null) { 369 return $this->resource; 370 } 371 if ($this->parent !== null) { 372 return $this->parent->getResource(); 373 } 374 return null; 375 } 376 377 /** 378 * For which resources is the metadata for 379 * @param ResourceCombo $resource 380 * @return $this 381 */ 382 public function setResource(ResourceCombo $resource): Metadata 383 { 384 $this->resource = $resource; 385 return $this; 386 } 387 388 /** 389 * @return string - the name use in the store 390 * For instance, a {@link PageImagePath} has a unique name of `page-image-path` 391 * but when we store it hierarchically, the prefix `page-image` is not needed 392 * and becomes simple `path` 393 */ 394 public static function getPersistentName(): string 395 { 396 return static::getName(); 397 } 398 399 /** 400 * @return string the name of the metadata (property) 401 * Used in all store such as database (therefore no minus please) 402 * Alphanumeric 403 */ 404 public static abstract function getName(): string; 405 406 407 /** 408 * @return string|array|null the value to be persisted by the store 409 * the reverse action is {@link Metadata::setFromStoreValue()} 410 */ 411 public function toStoreValue() 412 { 413 return $this->getValue(); 414 } 415 416 417 /** 418 * @return mixed 419 * The store default value is used to 420 * see if the value set is the same than the default one 421 * It this is the case, the data is not stored 422 */ 423 public function toStoreDefaultValue() 424 { 425 return $this->getDefaultValue(); 426 } 427 428 /** 429 * Data that should persist (this data should be in a backup) 430 */ 431 public const PERSISTENT_METADATA = "persistent"; 432 /** 433 * Data that are derived from other and stored 434 */ 435 public const DERIVED_METADATA = "derived"; 436 /** 437 * A runtime metadata is created for the purpose of a process 438 * Example {@link CacheRuntimeDependencies2} 439 */ 440 const RUNTIME_METADATA = "runtime"; 441 442 443 /** 444 * 445 * Return the type of metadata. 446 * * {@link Metadata::PERSISTENT_METADATA} 447 * * {@link Metadata::DERIVED_METADATA} 448 * * {@link Metadata::RUNTIME_METADATA} 449 * * 450 * 451 * Backup: Only the {@link Metadata::PERSISTENT_METADATA} got a backup 452 * 453 * @return string 454 * 455 * Unfortunately, Dokuwiki makes this distinction only in rendering 456 * https://forum.dokuwiki.org/d/19764-how-to-test-a-current-metadata-setting 457 * Therefore all metadata are persistent 458 * 459 * Ie a {@link MetadataDokuWikiStore::CURRENT_METADATA} is only derived 460 * in a rendering context. A {@link MetadataDokuWikiStore::PERSISTENT_METADATA} is always stored. 461 * 462 * 463 * 464 */ 465 public abstract function getPersistenceType(): string; 466 467 468 /** 469 * The user can't delete this metadata 470 * in the persistent metadata 471 */ 472 const NOT_MODIFIABLE_PERSISTENT_METADATA = [ 473 PagePath::PROPERTY_NAME, 474 PageCreationDate::PROPERTY_NAME, 475 ModificationDate::PROPERTY_NAME, 476 PageId::PROPERTY_NAME, 477 "user", 478 "contributor", 479 "creator", 480 "date", 481 action_plugin_combo_metadescription::DESCRIPTION_META_KEY, // Dokuwiki implements it as an array (you can't modify it directly) 482 "last_change" // not sure why it's in the persistent data 483 ]; 484 485 /** 486 * Metadata that we can lose 487 * because they are generated 488 * 489 * They still needs to be saved in as persistent metadata 490 * otherwise they are just not persisted 491 * https://forum.dokuwiki.org/d/19764-how-to-test-a-current-metadata-setting 492 */ 493 const RUNTIME_META = [ 494 "format", 495 "internal", // toc, cache, ... 496 "relation", 497 PageH1::H1_PARSED, 498 LowQualityCalculatedIndicator::LOW_QUALITY_INDICATOR_CALCULATED 499 ]; 500 501 502 /** 503 * The meta that are modifiable in the form. 504 * 505 * This meta could be replicated 506 * * in the {@link \syntax_plugin_combo_frontmatter} 507 * * or in the database 508 */ 509 const MUTABLE_METADATA = [ 510 Canonical::PROPERTY_NAME, 511 PageType::PROPERTY_NAME, 512 PageH1::PROPERTY_NAME, 513 Aliases::PROPERTY_NAME, 514 PageImages::PROPERTY_NAME, 515 Region::PROPERTY_NAME, 516 Lang::PROPERTY_NAME, 517 PageTitle::PROPERTY_NAME, 518 PagePublicationDate::PROPERTY_NAME, 519 ResourceName::PROPERTY_NAME, 520 LdJson::PROPERTY_NAME, 521 PageLayout::PROPERTY_NAME, 522 StartDate::PROPERTY_NAME, 523 EndDate::PROPERTY_NAME, 524 PageDescription::PROPERTY_NAME, 525 DisqusIdentifier::PROPERTY_NAME, 526 Slug::PROPERTY_NAME, 527 PageKeywords::PROPERTY_NAME, 528 CacheExpirationFrequency::PROPERTY_NAME, 529 QualityDynamicMonitoringOverwrite::PROPERTY_NAME, 530 LowQualityPageOverwrite::PROPERTY_NAME, 531 ]; 532 533 534 /** 535 * Delete the managed metadata 536 * @param $metadataArray - a metadata array 537 * @return array - the metadata array without the managed metadata 538 */ 539 public static function deleteMutableMetadata($metadataArray): array 540 { 541 if (sizeof($metadataArray) === 0) { 542 return $metadataArray; 543 } 544 $cleanedMetadata = []; 545 foreach ($metadataArray as $key => $value) { 546 if (!in_array($key, Metadata::MUTABLE_METADATA)) { 547 $cleanedMetadata[$key] = $value; 548 } 549 } 550 return $cleanedMetadata; 551 } 552 553 /** 554 * This function will upsert the meta array 555 * with a unique property 556 * @param $metaArray 557 * @param string $uniqueAttribute 558 * @param array $attributes 559 */ 560 public static function upsertMetaOnUniqueAttribute(&$metaArray, string $uniqueAttribute, array $attributes) 561 { 562 563 foreach ($metaArray as $key => $meta) { 564 if (!is_numeric($key)) { 565 LogUtility::msg("The passed array is not a meta array because the index are not numeric. Unable to update it."); 566 return; 567 } 568 if (isset($meta[$uniqueAttribute])) { 569 $value = $meta[$uniqueAttribute]; 570 if ($value === $attributes[$uniqueAttribute]) { 571 $metaArray[$key] = $attributes; 572 return; 573 } 574 } 575 } 576 $metaArray[] = $attributes; 577 578 } 579 580 /** 581 * Delete the runtime if present 582 * (They were saved in persistent) 583 */ 584 public static function deleteIfPresent(array &$persistentPageMeta, array $attributeToDeletes): bool 585 { 586 $unsetWasPerformed = false; 587 foreach ($attributeToDeletes as $runtimeMeta) { 588 if (isset($persistentPageMeta[$runtimeMeta])) { 589 unset($persistentPageMeta[$runtimeMeta]); 590 $unsetWasPerformed = true; 591 } 592 } 593 return $unsetWasPerformed; 594 } 595 596 /** 597 * @return bool can the user change the value 598 * In a form, the field will be disabled 599 */ 600 public abstract function getMutable(): bool; 601 602 603 /** 604 * @return array|null The possible values that can take this metadata 605 * If null, all, no constraints 606 */ 607 public function getPossibleValues(): ?array 608 { 609 return null; 610 } 611 612 /** 613 * An utility function to {@link Metadata::sendToWriteStore()} 614 * and {@link MetadataStore::persist()} at the same time in the {@link Metadata::getWriteStore() write store} 615 * @throws ExceptionCombo 616 */ 617 public function persist(): Metadata 618 { 619 $this->sendToWriteStore(); 620 $this->getWriteStore()->persist(); 621 return $this; 622 } 623 624 /** 625 * Build the object from the store value 626 * 627 * The inverse function is {@link Metadata::toStoreValue()} 628 * 629 * The function used by {@link Metadata::buildFromReadStore()} 630 * to build the value from the {@link MetadataStore::get()} 631 * function. 632 * 633 * The difference between the {@link Metadata::setFromStoreValue()} 634 * is that this function should not make any validity check 635 * or throw any exception 636 * 637 * @param $value 638 * @return mixed 639 */ 640 public abstract function buildFromStoreValue($value): Metadata; 641 642 /** 643 * If you have quality problem to throw, you can use this function 644 * instead of {@link Metadata::buildFromStoreValue()} 645 * @param $value 646 * @return Metadata 647 */ 648 public function setFromStoreValue($value): Metadata 649 { 650 return $this->buildFromStoreValue($value); 651 } 652 653 654 /** 655 * The class of an entity metadata (ie if the metadata has children / is a {@link Metadata::$parent} 656 * 657 * One id value = one row = one entity 658 * 659 * @return string|null 660 */ 661 public function getUidClass(): ?string 662 { 663 if ($this->getChildrenClass() !== null) { 664 LogUtility::msg("An entity metadata should define a metadata that store the unique value"); 665 } 666 return null; 667 } 668 669 /** 670 * The width on a scale of 12 for the form field 671 * @return null 672 */ 673 public function getFormControlWidth() 674 { 675 return null; 676 } 677 678 /** 679 * @return string[] - the old name if any 680 */ 681 public static function getOldPersistentNames(): array 682 { 683 return []; 684 } 685 686 /** 687 * @return mixed - the memory value 688 */ 689 public abstract function getValue(); 690 691 public abstract function getDefaultValue(); 692 693 /** 694 * @return mixed - set the memory value from the store and return ut 695 */ 696 public function getValueFromStore() 697 { 698 $this->buildFromStoreValue($this->getReadStore()->get($this)); 699 return $this->getValue(); 700 } 701 702 703 public function getValueFromStoreOrDefault() 704 { 705 $this->buildFromStoreValue($this->getReadStore()->get($this)); 706 return $this->getValueOrDefault(); 707 } 708 709 public function getValueOrDefault() 710 { 711 712 $value = $this->getValue(); 713 if ($value === null || $value === "") { 714 return $this->getDefaultValue(); 715 } 716 return $value; 717 718 } 719 720 721 /** 722 * @return MetadataStore - the store where the metadata are persist (by default, the {@link Metadata::getReadStore()} 723 */ 724 public function getWriteStore(): MetadataStore 725 { 726 if ($this->writeStore === null) { 727 return $this->getReadStore(); 728 } 729 if (!$this->writeStore instanceof MetadataStore) { 730 $this->writeStore = MetadataStoreAbs::toMetadataStore($this->writeStore, $this->getResource()); 731 } 732 return $this->writeStore; 733 } 734 735 public function getUidObject(): Metadata 736 { 737 if ($this->uidObject === null) { 738 $this->uidObject = Metadata::toMetadataObject($this->getUidClass()) 739 ->setResource($this->getResource()); 740 } 741 return $this->uidObject; 742 } 743 744} 745