2021-08-25 04:29:26 +09:00
|
|
|
<?php
|
|
|
|
|
2021-10-10 17:26:18 +09:00
|
|
|
declare(strict_types = 1);
|
|
|
|
|
2021-08-25 04:29:26 +09:00
|
|
|
/*
|
|
|
|
* This file is part of the ActivityPhp package.
|
|
|
|
*
|
|
|
|
* Copyright (c) landrok at github.com/landrok
|
|
|
|
*
|
|
|
|
* For the full copyright and license information, please see
|
|
|
|
* <https://github.com/landrok/activitypub/blob/master/LICENSE>.
|
|
|
|
*/
|
|
|
|
|
2021-10-05 01:00:58 +09:00
|
|
|
namespace Plugin\ActivityPub\Util\Type;
|
2021-08-25 04:29:26 +09:00
|
|
|
|
|
|
|
use Exception;
|
2021-10-05 01:00:58 +09:00
|
|
|
use Plugin\ActivityPub\Util\Type;
|
2021-08-25 04:29:26 +09:00
|
|
|
use ReflectionClass;
|
2021-10-10 17:26:18 +09:00
|
|
|
use ReflectionProperty;
|
2021-08-25 04:29:26 +09:00
|
|
|
|
|
|
|
/**
|
|
|
|
* \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 = [];
|
|
|
|
|
2021-09-07 04:59:36 +09:00
|
|
|
protected string $type = 'AbstractObject';
|
|
|
|
|
2021-08-25 04:29:26 +09:00
|
|
|
/**
|
|
|
|
* 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,
|
2021-10-10 17:26:18 +09:00
|
|
|
print_r($value, true),
|
2021-08-25 04:29:26 +09:00
|
|
|
)
|
2021-10-10 17:26:18 +09:00
|
|
|
. \PHP_EOL,
|
2021-08-25 04:29:26 +09:00
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
// @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
|
2021-10-10 17:26:18 +09:00
|
|
|
if (\is_array($value)) {
|
2021-08-25 04:29:26 +09:00
|
|
|
if (isset($value['type'])) {
|
|
|
|
return Type::create($value);
|
2021-10-10 17:26:18 +09:00
|
|
|
} elseif (\is_int(key($value))) {
|
2021-08-25 04:29:26 +09:00
|
|
|
return array_map(
|
|
|
|
static function ($value) {
|
2021-10-10 17:26:18 +09:00
|
|
|
return \is_array($value) && isset($value['type'])
|
2021-08-25 04:29:26 +09:00
|
|
|
? Type::create($value)
|
|
|
|
: $value;
|
|
|
|
},
|
2021-10-10 17:26:18 +09:00
|
|
|
$value,
|
2021-08-25 04:29:26 +09:00
|
|
|
);
|
|
|
|
// 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})) {
|
2021-10-10 17:26:18 +09:00
|
|
|
if (!\array_key_exists($name, $this->_props)) {
|
2021-08-25 04:29:26 +09:00
|
|
|
$this->_props[$name] = $this->{$name};
|
|
|
|
}
|
|
|
|
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
2021-10-10 17:26:18 +09:00
|
|
|
if (\array_key_exists($name, $this->_props)) {
|
2021-08-25 04:29:26 +09:00
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
|
|
|
$reflect = new ReflectionClass(Type::create($this->type));
|
2021-10-10 17:26:18 +09:00
|
|
|
$allowed_props = $reflect->getProperties(ReflectionProperty::IS_PUBLIC | ReflectionProperty::IS_PROTECTED);
|
2021-08-25 04:29:26 +09:00
|
|
|
$allowed = [];
|
|
|
|
foreach ($allowed_props as $prop) {
|
|
|
|
$allowed[] = $prop->getName();
|
|
|
|
}
|
2021-10-10 17:26:18 +09:00
|
|
|
if (!\in_array($name, $allowed)) {
|
2021-08-25 04:29:26 +09:00
|
|
|
sort($allowed);
|
|
|
|
throw new Exception(
|
|
|
|
sprintf(
|
2021-10-10 17:26:18 +09:00
|
|
|
'Property "%s" is not defined. Type="%s", '
|
|
|
|
. 'Class="%s"' . \PHP_EOL . 'Allowed properties: %s',
|
2021-08-25 04:29:26 +09:00
|
|
|
$name,
|
|
|
|
$this->get('type'),
|
|
|
|
static::class,
|
2021-10-10 17:26:18 +09:00
|
|
|
implode(', ', $allowed),
|
|
|
|
),
|
2021-08-25 04:29:26 +09:00
|
|
|
);
|
|
|
|
} 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),
|
2021-10-10 17:26:18 +09:00
|
|
|
['_props' => '1'],
|
|
|
|
),
|
|
|
|
),
|
|
|
|
),
|
|
|
|
),
|
2021-08-25 04:29:26 +09:00
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* 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),
|
2021-10-10 17:26:18 +09:00
|
|
|
static fn ($value, $key): bool => !\is_null($value) && $key !== '_props',
|
|
|
|
\ARRAY_FILTER_USE_BOTH,
|
|
|
|
),
|
2021-08-25 04:29:26 +09:00
|
|
|
);
|
|
|
|
|
|
|
|
$stack = [];
|
|
|
|
|
|
|
|
// native properties
|
|
|
|
foreach ($keys as $key) {
|
|
|
|
if ($this->{$key} instanceof self) {
|
|
|
|
$stack[$key] = $this->{$key}->toArray();
|
2021-10-10 17:26:18 +09:00
|
|
|
} elseif (!\is_array($this->{$key})) {
|
2021-08-25 04:29:26 +09:00
|
|
|
$stack[$key] = $this->{$key};
|
2021-10-10 17:26:18 +09:00
|
|
|
} elseif (\is_array($this->{$key})) {
|
|
|
|
if (\is_int(key($this->{$key}))) {
|
2021-08-25 04:29:26 +09:00
|
|
|
$stack[$key] = array_map(
|
|
|
|
static function ($value) {
|
|
|
|
return $value instanceof self
|
|
|
|
? $value->toArray()
|
|
|
|
: $value;
|
|
|
|
},
|
2021-10-10 17:26:18 +09:00
|
|
|
$this->{$key},
|
2021-08-25 04:29:26 +09:00
|
|
|
);
|
|
|
|
} else {
|
|
|
|
$stack[$key] = $this->{$key};
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// _props
|
|
|
|
foreach ($this->_props as $key => $value) {
|
2021-10-10 17:26:18 +09:00
|
|
|
if (\is_null($value)) {
|
2021-08-25 04:29:26 +09:00
|
|
|
continue;
|
|
|
|
}
|
|
|
|
|
|
|
|
if ($value instanceof self) {
|
|
|
|
$stack[$key] = $value->toArray();
|
2021-10-10 17:26:18 +09:00
|
|
|
} elseif (!\is_array($value)) {
|
2021-08-25 04:29:26 +09:00
|
|
|
$stack[$key] = $value;
|
|
|
|
} else {
|
2021-10-10 17:26:18 +09:00
|
|
|
if (\is_int(key($value))) {
|
2021-08-25 04:29:26 +09:00
|
|
|
$stack[$key] = array_map(
|
|
|
|
static function ($value) {
|
|
|
|
return $value instanceof self
|
|
|
|
? $value->toArray()
|
|
|
|
: $value;
|
|
|
|
},
|
2021-10-10 17:26:18 +09:00
|
|
|
$value,
|
2021-08-25 04:29:26 +09:00
|
|
|
);
|
|
|
|
} 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(),
|
2021-10-10 17:26:18 +09:00
|
|
|
(int) $options,
|
2021-08-25 04:29:26 +09:00
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* 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,
|
2021-10-10 17:26:18 +09:00
|
|
|
$this->toArray(),
|
2021-08-25 04:29:26 +09:00
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Extend current type properties
|
|
|
|
*
|
2021-10-10 17:26:18 +09:00
|
|
|
* @param mixed $default
|
2021-08-25 04:29:26 +09:00
|
|
|
*
|
|
|
|
* @throws Exception
|
|
|
|
*/
|
|
|
|
public function extend(string $property, mixed $default = null): void
|
|
|
|
{
|
|
|
|
if ($this->has($property)) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2021-10-10 17:26:18 +09:00
|
|
|
if (!\array_key_exists($property, $this->_props)) {
|
2021-08-25 04:29:26 +09:00
|
|
|
$this->_props[$property] = $default;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Magical isset method
|
|
|
|
*/
|
|
|
|
public function __isset(string $name): bool
|
|
|
|
{
|
|
|
|
return property_exists($this, $name)
|
2021-10-10 17:26:18 +09:00
|
|
|
|| \array_key_exists($name, $this->_props);
|
2021-08-25 04:29:26 +09:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* 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
|
|
|
|
*/
|
2021-10-10 17:26:18 +09:00
|
|
|
public function __call(string $name, ?array $arguments = []): mixed
|
2021-08-25 04:29:26 +09:00
|
|
|
{
|
|
|
|
// Getters
|
|
|
|
if (str_starts_with($name, 'get')) {
|
2021-10-10 17:26:18 +09:00
|
|
|
$attr = lcfirst(mb_substr($name, 3));
|
2021-08-25 04:29:26 +09:00
|
|
|
return $this->get($attr);
|
|
|
|
}
|
|
|
|
|
|
|
|
// Setters
|
|
|
|
if (str_starts_with($name, 'set')) {
|
2021-10-10 17:26:18 +09:00
|
|
|
if (\count($arguments) === 1) {
|
|
|
|
$attr = lcfirst(mb_substr($name, 3));
|
2021-08-25 04:29:26 +09:00
|
|
|
return $this->set($attr, $arguments[0]);
|
|
|
|
} else {
|
|
|
|
throw new Exception(
|
|
|
|
sprintf(
|
|
|
|
'Expected exactly one argument for method "%s()"',
|
2021-10-10 17:26:18 +09:00
|
|
|
$name,
|
|
|
|
),
|
2021-08-25 04:29:26 +09:00
|
|
|
);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
throw new Exception(
|
|
|
|
sprintf(
|
|
|
|
'Method "%s" is not defined',
|
2021-10-10 17:26:18 +09:00
|
|
|
$name,
|
|
|
|
),
|
2021-08-25 04:29:26 +09:00
|
|
|
);
|
|
|
|
}
|
|
|
|
}
|