* @package taoQTI */ abstract class Element implements Exportable { protected $serial = ''; protected $relatedItem = null; private static $instances = []; /** * Short description of attribute templatesPath * * @access protected * @var string */ protected static $templatesPath = ''; /** * the QTI tag name as defined in QTI standard * * @access protected * @var string */ protected static $qtiTagName = ''; /** * the options of the element * * @access protected * @var array */ protected $attributes = []; public function __construct($attributes = [], Item $relatedItem = null, $serial = '') { if (!is_null($relatedItem)) { $this->setRelatedItem($relatedItem); } if (!empty($serial)) { //try setting object serial manually: if (isset(self::$instances[$this->getSerial()])) { throw new QtiModelException('the serial must be unique'); } else { $this->serial = $serial; } } else { $this->getSerial(); //generate one } $this->resetAttributes(); $this->setAttributes($attributes); self::$instances[$this->getSerial()] = $this; } /** * Provide the list of attributes of the Qti Element Class */ abstract protected function getUsedAttributes(); /** * Reset the attributes values to the default values defined by the standard */ public function resetAttributes() { $this->attributes = []; foreach ($this->getUsedAttributes() as $attributeClass) { if (class_exists($attributeClass) && is_subclass_of($attributeClass, 'oat\\taoQtiItem\\model\\qti\\attribute\\Attribute')) { $attribute = new $attributeClass(); $this->attributes[$attribute->getName()] = $attribute; } else { common_Logger::w('attr does not exists ' . $attributeClass); } } } public function getQtiTag() { return static::$qtiTagName; } /** * Remove the actual value of an attribute, distinguish from empty value * * @param string $name */ public function removeAttributeValue($name) { if (isset($this->attributes[$name])) { $this->attributes[$name]->setNull(); } } /** * Set the attributes for the the Qti Element * Argument format: array(attributeName => value) * * @param array $values * @throws InvalidArgumentException */ public function setAttributes($values) { if (is_array($values)) { foreach ($values as $name => $value) { $this->setAttribute($name, $value); } } else { throw new InvalidArgumentException('"values" must be an array'); } } /** * Set the value of an attribute * * @param string $name * @param mixed $value * @return boolean * @throws InvalidArgumentException * @throws \oat\taoQtiItem\model\qti\exception\QtiModelException */ public function setAttribute($name, $value) { $returnValue = false; if (is_null($value)) { return $returnValue; } if (isset($this->attributes[$name])) { $datatypeClass = $this->attributes[$name]->getType(); // check if the attribute needs an element level validation if (is_subclass_of($datatypeClass, 'oat\\taoQtiItem\\model\\qti\\datatype\\Identifier')) { if ($value instanceof IdentifiedElement) { if ($this->validateAttribute($name, $value)) { $this->attributes[$name]->setValue($value); $returnValue = true; } else { $vr = print_r($value, true); common_Logger::w($vr); throw new InvalidArgumentException('Invalid identifier attribute value'); } } elseif (is_string($value)) { // try converting to string identifier and search the identified object: $identifier = (string) $value; $elt = $this->getIdentifiedElement($identifier, $datatypeClass::getAllowedClasses()); if (!is_null($elt)) { // ok, found among allowed classes $this->attributes[$name]->setValue($elt); $returnValue = true; } else { throw new QtiModelException('No QTI element with the identifier has been found: ' . $identifier); } } } else { $this->attributes[$name]->setValue($value); $returnValue = true; } } else { $this->attributes[$name] = new Generic($value); $returnValue = true; } return $returnValue; } /** * Validate an attribute of the element, at the element level * (the validator of the attributes are on the attribute level) * * @param string $name * @param mixed $value * @return boolean * @throws \oat\taoQtiItem\model\qti\exception\QtiModelException */ public function validateAttribute($name, $value = null) { $returnValue = false; if (isset($this->attributes[$name])) { if (is_null($value)) { $value = $this->attributes[$name]->getValue(); } $datatypeClass = $this->attributes[$name]->getType(); if (is_subclass_of($datatypeClass, 'oat\\taoQtiItem\\model\\qti\\datatype\\Identifier')) { } else { $returnValue = $datatypeClass::validate($value); } if (is_subclass_of($datatypeClass, 'oat\\taoQtiItem\\model\\qti\\datatype\\Identifier')) { if ($datatypeClass::validate($value)) { // validate itentifier $relatedItem = $this->getRelatedItem(); if (!is_null($relatedItem)) { $idCollection = $relatedItem->getIdentifiedElements(); if ($value instanceof IdentifiedElement && $idCollection->exists($value->getIdentifier())) { $returnValue = true; } } else { common_Logger::w('iden'); throw new QtiModelException('Cannot verify identifier reference because the element is not in a QTI Item ' . get_class($this) . '::' . $name, 0); } } } else { $returnValue = $datatypeClass::validate($value); } } else { throw new InvalidArgumentException('no attribute found with the name "' . $name . '"'); } return $returnValue; } /** * Find the identified object corresponding to the identifier string * The optional argument $elementClasses search a specific QTI element class * * @param string $identifier * @param array $elementClasses * @return \oat\taoQtiItem\model\qti\IdentifiedElement */ public function getIdentifiedElement($identifier, $elementClasses = []) { $returnValue = null; if (!is_array($elementClasses)) { throw new InvalidArgumentException('elementClasses must be an array'); } $relatedItem = $this->getRelatedItem(); if (!is_null($relatedItem)) { $identifiedElementsCollection = $relatedItem->getIdentifiedElements(); if (empty($elementClasses)) { $returnValue = $identifiedElementsCollection->getUnique($identifier); } else { foreach ($elementClasses as $elementClass) { $returnValue = $identifiedElementsCollection->getUnique($identifier, $elementClass); if (!is_null($returnValue)) { break; } } } } return $returnValue; } /** * Check if an attribute exists within the Qti Element * * @param string $name * @return boolean */ public function hasAttribute($name) { return isset($this->attributes[$name]); } /** * Short handy method to get/set an attribute value * * @param string $name * @param mixed $value * @return mixed */ public function attr($name, $value = null) { if (is_null($value)) { return $this->getAttributeValue($name); } else { return $this->setAttribute($name, $value); } } /** * Add a CSS class to the item body * * @author Dieter Raber * @param $className one or more class names, separated by space */ public function addClass($className) { $oldClassName = $this->getAttributeValue('class'); $oldClassNameArr = $oldClassName ? explode(' ', $oldClassName) : []; $classNameArr = array_merge($oldClassNameArr, explode(' ', $className)); // housekeeping $classNameArr = array_unique(array_filter(array_map('trim', $classNameArr))); $this->setAttribute('class', implode(' ', $classNameArr)); } /** * Add a CSS class from the item body * * @author Dieter Raber * @param $className */ public function removeClass($className) { $oldClassName = $this->getAttributeValue('class'); $oldClassNameArr = $oldClassName ? explode(' ', $oldClassName) : []; unset($oldClassNameArr[array_search($className, $oldClassNameArr)]); $this->setAttribute('class', implode(' ', $oldClassNameArr)); } /** * Get the attribute as an Attribute object * * @param type $name * @return \oat\taoQtiItem\model\qti\attribute\Attribute */ protected function getAttribute($name) { return $this->hasAttribute($name) ? $this->attributes[$name] : null; } /** * Get the attribute's actual value (not as an Attribute object) * * @param string $name * @return mixed */ public function getAttributeValue($name) { $returnValue = null; if ($this->hasAttribute($name)) { $returnValue = $this->attributes[$name]->getValue(); } return $returnValue; } /** * Get all attributes' values * * @return array */ public function getAttributeValues($filterNull = true) { $returnValue = []; foreach ($this->attributes as $name => $attribute) { if (!$filterNull || !$attribute->isNull()) { $returnValue[$name] = $attribute->getValue(); } } return $returnValue; } /** * Get the placeholder of the Qti Element to used in a Container * * @see oat\taoQtiItem\model\qti\container\Container * @return string */ public function getPlaceholder() { return '{{' . $this->getSerial() . '}}'; } /** * Get the absolute path of the template of the qti.xml * * @return string * @throws \oat\taoQtiItem\model\qti\exception\QtiModelException */ public static function getTemplateQti() { if (empty(static::$qtiTagName)) { throw new QtiModelException('The element has no tag name defined : ' . get_called_class()); } $template = static::getTemplatePath() . '/qti.' . static::$qtiTagName . '.tpl.php'; if (!file_exists($template)) { $template = static::getTemplatePath() . '/qti.element.tpl.php'; } return $template; } /** * Get the variables to be used in the qti.xml template * * @return array */ protected function getTemplateQtiVariables() { $variables = []; $variables['tag'] = static::$qtiTagName; $variables['attributes'] = $this->getAttributeValues(); if ($this instanceof FlowContainer) { $variables['body'] = $this->getBody()->toQTI(); } return $variables; } /** * Export the data to the QTI XML format * * @return string */ public function toQTI() { $template = static::getTemplateQti(); $variables = $this->getTemplateQtiVariables(); if (isset($variables['attributes'])) { $variables['attributes'] = $this->xmlizeOptions($variables['attributes'], true); } $tplRenderer = new taoItems_models_classes_TemplateRenderer($template, $variables); $returnValue = $tplRenderer->render(); return (string) $returnValue; } /** * Get the array representation of the Qti Element. * Particularly helpful for data transformation, e.g. json * * @param $filterVariableContent * @param array $filtered * @return array */ public function toArray($filterVariableContent = false, &$filtered = []) { $data = []; $data['serial'] = $this->getSerial(); $tag = $this->getQtiTag(); if (!empty($tag)) { $data['qtiClass'] = $tag; } $attributes = $this->getAttributeValues(); $data['attributes'] = empty($attributes) ? new StdClass() : $attributes; if ($this instanceof FlowContainer) { $data['body'] = $this->getBody()->toArray($filterVariableContent, $filtered); } if ($this->isDebug()) { //in debug mode, add debug data, such as the related item $data['debug'] = ['relatedItem' => is_null($this->getRelatedItem()) ? '' : $this->getRelatedItem()->getSerial()]; } return $data; } /** * Get the main template directory * * @access public * @author Sam, * @return string */ public static function getTemplatePath() { if (empty(self::$templatesPath)) { $dir = \common_ext_ExtensionsManager::singleton()->getExtensionById('taoQtiItem')->getDir(); self::$templatesPath = $dir . 'model/qti/templates/'; } $returnValue = self::$templatesPath; return (string) $returnValue; } /** * Set the item the current Qti Element belongs to. * The related item assignment is propagated to all containing Qti Element of the current one. * The "force" option allows changing the associated item (even if it has already been defined) * * @param \oat\taoQtiItem\model\qti\Item $item * @param boolean $force * @return boolean * @throws \oat\taoQtiItem\model\qti\exception\QtiModelException */ public function setRelatedItem(Item $item, $force = false) { $returnValue = false; if (!is_null($this->relatedItem) && $this->relatedItem->getSerial() == $item->getSerial()) { $returnValue = true; // identical } elseif (!$force && !is_null($this->relatedItem)) { throw new QtiModelException('attempt to change item reference for a QTI element'); } else { // propagate the assignation of item to all included objects $reflection = new ReflectionClass($this); foreach ($reflection->getProperties() as $property) { if (!$property->isStatic() && !$property->isPrivate()) { $propertyName = $property->getName(); $value = $this->$propertyName; if (is_array($value)) { foreach ($value as $subvalue) { if (is_object($subvalue) && $subvalue instanceof Element) { $subvalue->setRelatedItem($item); } elseif (is_object($subvalue) && $subvalue instanceof ResponseIdentifier) { // manage the reference of identifier $idenfierBaseType = $subvalue->getValue(true); if (!is_null($idenfierBaseType)) { $idenfierBaseType->getReferencedObject()->setRelatedItem($item); } } } } elseif (is_object($value) && $value instanceof Element) { $value->setRelatedItem($item); } } } // set item reference to current object $this->relatedItem = $item; $returnValue = true; } return $returnValue; } /** * Recursively get all Qti Elements contained within the current Qti Element * * @param string $className * @return array */ public function getComposingElements($className = '') { $returnValue = []; if ($className === '') { $className = 'oat\taoQtiItem\model\qti\Element'; } $reflection = new ReflectionClass($this); foreach ($reflection->getProperties() as $property) { if (!$property->isStatic() && !$property->isPrivate()) { $propertyName = $property->getName(); if ($propertyName != 'relatedItem') { $value = $this->$propertyName; if (is_array($value)) { foreach ($value as $subvalue) { if ($subvalue instanceof Element) { if ($subvalue instanceof $className) { $returnValue[$subvalue->getSerial()] = $subvalue; } $returnValue = array_merge($returnValue, $subvalue->getComposingElements($className)); } } } else { if ($value instanceof Element) { if ($value instanceof $className) { if ($value->getSerial() != $this->getSerial()) { $returnValue[$value->getSerial()] = $value; } } $returnValue = array_merge($returnValue, $value->getComposingElements($className)); } } } } } return $returnValue; } /** * Get the Qti Item the current Qti Element belongs to * * @return \oat\taoQtiItem\model\qti\Item */ public function getRelatedItem() { return $this->relatedItem; } /** * This method enables you to build a string of attributes for an xml node * from the Qti Element attributes according to their types. * * @access protected * @author Sam, * @param array formalOpts * @param boolean recursive * @return string */ protected function xmlizeOptions($formalOpts = [], $recursive = false) { $returnValue = (string) ''; if (!is_array($formalOpts)) { throw new InvalidArgumentException('formalOpts must be an array, ' . gettype($formalOpts) . ' given'); } $options = (!$recursive) ? $this->getAttributeValues() : $formalOpts; foreach ($options as $key => $value) { if (is_string($value) || is_numeric($value)) { // str_replace is unicode safe... $returnValue .= ' ' . $key . '="' . str_replace([ '&', '<', '>', '\'', '"' ], [ '&', '<', '>', ''', '"' ], $value) . '"'; } if (is_bool($value)) { $returnValue .= ' ' . $key . '="' . (($value) ? 'true' : 'false') . '"'; } if (is_array($value)) { if (count($value) > 0) { $keys = array_keys($value); if (is_int($keys[0])) { // repeat the attribute key $returnValue .= ' ' . $key . '="' . implode(' ', array_values($value)) . '"'; } else { $returnValue .= $this->xmlizeOptions($value, true); } } } } return (string) $returnValue; } /** * Obtain a serial for the instance of the class that implements the * * @access public * @author Jerome Bogaerts, * @return string */ public function getSerial() { if (empty($this->serial)) { $this->serial = $this->buildSerial(); } $returnValue = $this->serial; return (string) $returnValue; } /** * create a unique serial number * * @access protected * @author Sam, * @return string */ protected function buildSerial() { if ($this->isDebug()) { //in debug mode, use more meaningful serials $clazz = strtolower(get_class($this)); $prefix = substr($clazz, strpos($clazz, 'taoqtiitem\\model\\qti\\') + 21) . '_'; $serial = str_replace('.', '', uniqid($prefix, true)); $serial = str_replace('\\', '_', $serial); } else { //build a short unique id for memory saving $serial = uniqid('i'); } return (string) $serial; } protected function getArraySerializedElementCollection($elements, $filterVariableContent = false, &$filtered = []) { if (empty($elements)) { $data = new stdClass(); } else { $data = []; foreach ($elements as $element) { $data[$element->getSerial()] = $element->toArray($filterVariableContent, $filtered); } } return $data; } protected function getArraySerializedPrimitiveCollection($elements) { if (empty($elements)) { $data = new stdClass(); } else { $data = []; foreach ($elements as $key => $value) { if (is_array($value)) { $data[$key] = $this->getArraySerializedPrimitiveCollection($value); } else { $data[$key] = $value; } } } return $data; } /** * @deprecated we should not use global constant to debug classes */ private function isDebug(): bool { if (defined('DEBUG_MODE')) { return DEBUG_MODE; } return false; } }