. */ namespace Plugin\ActivityPub\Util\Type; use Exception; use Plugin\ActivityPub\Util\Type; use ReflectionClass; use ReflectionProperty; /** * \ActivityPhp\Type\ObjectAbstract is an abstract class for all * Activity Streams Core Types. * * @see https://www.w3.org/TR/activitystreams-core/#model */ abstract class AbstractObject { /** * Keep all properties values that have been set */ private array $_props = []; protected string $type = 'AbstractObject'; /** * Standard setter method * - Perform content validation if a validator exists * * @throws Exception * * @return $this */ public function set(string $name, mixed $value): static { // Throws an exception when property is undefined if ($name !== '@context') { $this->has($name); } // Validate given value if (!Validator::validate($name, $value, $this)) { $message = "Rejected value. Type='%s', Property='%s', value='%s'"; throw new Exception( sprintf( $message, static::class, $name, print_r($value, true), ) . \PHP_EOL, ); } // @context has a special role if ($name === '@context') { $this->_props[$name] = $value; // All modes and property defined } elseif ($this->has($name)) { $this->_props[$name] = $this->transform($value); // Undefined property but it's valid as it was // tested in the if clause above (no exception) so, let's include it } else { $this->_props[$name] = $this->transform($value); } return $this; } /** * Affect a value to a property or an extended property * * @throws Exception */ private function transform(mixed $value): mixed { // Deep typing if (\is_array($value)) { if (isset($value['type'])) { return Type::create($value); } elseif (\is_int(key($value))) { return array_map( static function ($value) { return \is_array($value) && isset($value['type']) ? Type::create($value) : $value; }, $value, ); // Empty array, array that should not be cast as ActivityStreams types } else { return $value; } } else { // Scalars return $value; } } /** * Standard getter method * * @throws Exception */ public function get(string $name): mixed { // Throws an exception when property is undefined $this->has($name); return $this->_props[$name]; } /** * Checks that property exists * * @throws Exception */ public function has(string $name): bool { if (isset($this->{$name})) { if (!\array_key_exists($name, $this->_props)) { $this->_props[$name] = $this->{$name}; } return true; } if (\array_key_exists($name, $this->_props)) { return true; } $reflect = new ReflectionClass(Type::create($this->type)); $allowed_props = $reflect->getProperties(ReflectionProperty::IS_PUBLIC | ReflectionProperty::IS_PROTECTED); $allowed = []; foreach ($allowed_props as $prop) { $allowed[] = $prop->getName(); } if (!\in_array($name, $allowed)) { sort($allowed); throw new Exception( sprintf( 'Property "%s" is not defined. Type="%s", ' . 'Class="%s"' . \PHP_EOL . 'Allowed properties: %s', $name, $this->get('type'), static::class, implode(', ', $allowed), ), ); } else { return false; } } /** * Get a list of all properties names */ public function getProperties(): array { return array_values( array_unique( array_merge( array_keys($this->_props), array_keys( array_diff_key( get_object_vars($this), ['_props' => '1'], ), ), ), ), ); } /** * Get a list of all properties and their values * as an associative array. * Null values are not returned. */ public function toArray(): array { $keys = array_keys( array_filter( get_object_vars($this), static fn ($value, $key): bool => !\is_null($value) && $key !== '_props', \ARRAY_FILTER_USE_BOTH, ), ); $stack = []; // native properties foreach ($keys as $key) { if ($this->{$key} instanceof self) { $stack[$key] = $this->{$key}->toArray(); } elseif (!\is_array($this->{$key})) { $stack[$key] = $this->{$key}; } elseif (\is_array($this->{$key})) { if (\is_int(key($this->{$key}))) { $stack[$key] = array_map( static function ($value) { return $value instanceof self ? $value->toArray() : $value; }, $this->{$key}, ); } else { $stack[$key] = $this->{$key}; } } } // _props foreach ($this->_props as $key => $value) { if (\is_null($value)) { continue; } if ($value instanceof self) { $stack[$key] = $value->toArray(); } elseif (!\is_array($value)) { $stack[$key] = $value; } else { if (\is_int(key($value))) { $stack[$key] = array_map( static function ($value) { return $value instanceof self ? $value->toArray() : $value; }, $value, ); } else { $stack[$key] = $value; } } } return $stack; } /** * Get a JSON * * @param null|int $options PHP JSON options */ public function toJson(?int $options = null): string { return json_encode( $this->toArray(), (int) $options, ); } /** * Get a copy of current object and return a new instance * * @throws Exception * * @return self A new instance of this object */ public function copy(): self { return Type::create( $this->type, $this->toArray(), ); } /** * Extend current type properties * * @param mixed $default * * @throws Exception */ public function extend(string $property, mixed $default = null): void { if ($this->has($property)) { return; } if (!\array_key_exists($property, $this->_props)) { $this->_props[$property] = $default; } } /** * Magical isset method */ public function __isset(string $name): bool { return property_exists($this, $name) || \array_key_exists($name, $this->_props); } /** * Magical setter method * * @throws Exception */ public function __set(string $name, mixed $value): void { $this->set($name, $value); } /** * Magical getter method * * @throws Exception */ public function __get(string $name): mixed { return $this->get($name); } /** * Overloading methods * * @throws Exception */ public function __call(string $name, ?array $arguments = []): mixed { // Getters if (str_starts_with($name, 'get')) { $attr = lcfirst(mb_substr($name, 3)); return $this->get($attr); } // Setters if (str_starts_with($name, 'set')) { if (\count($arguments) === 1) { $attr = lcfirst(mb_substr($name, 3)); return $this->set($attr, $arguments[0]); } else { throw new Exception( sprintf( 'Expected exactly one argument for method "%s()"', $name, ), ); } } throw new Exception( sprintf( 'Method "%s" is not defined', $name, ), ); } }