Skip to content
Permalink
Branch: master
Find file Copy path
Find file Copy path
Fetching contributors…
Cannot retrieve contributors at this time
385 lines (338 sloc) 12.4 KB
<?php
declare(strict_types=1);
namespace snuze\Reddit\Thing;
/**
* The Thing class represents the common properties of all Reddit "things."
* Various subtypes of Thing exist to define the properties specific to those
* objects.
*
* You can't instantiate a generic Thing object; instead, you should create
* and use the various subtypes: Subreddit, Link, Account, Comment, etc.
*
* *****************************************************************************
* This file is part of Snuze, a PHP client for the Reddit API.
* Copyright 2019 Shaun Cummiskey <shaun@shaunc.com> <https://shaunc.com/>
* Repository: <https://github.com/snuze/snuze/>
* Documentation: <https://snuze.shaunc.com/>
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
abstract class Thing extends \snuze\SnuzeObject implements \snuze\Interfaces\Jsonable
{
/**
* Defines the kind discriminator for a comment
*/
const KIND_COMMENT = 't1';
/**
* Defines the kind discriminator for an account
*/
const KIND_ACCOUNT = 't2';
/**
* Defines the kind discriminator for a link (story, submission)
*/
const KIND_LINK = 't3';
/**
* Defines the kind discriminator for a message (direct message, modmail)
*/
const KIND_MESSAGE = 't4';
/**
* Defines the kind discriminator for a subreddit
*/
const KIND_SUBREDDIT = 't5';
/**
* Defines the kind discriminator for an award
*/
const KIND_AWARD = 't6';
/**
* Defines a valid Thing ID. This is the base36 ID only, without the t1, t2,
* t3, etc. "kind" prefix.
*/
const REGEX_VALID_ID = '|^[a-z0-9]{1,13}$|i';
/**
* A string that indicates what type of Thing this is; for example, 't1' to
* designate a comment. Corresponds to the KIND_ constants defined above.
* Used to build the Thing's fullname.
*
* @var string
*/
protected $_kind = null;
/**
* An array to hold the class property names. Each child class must define
* this in its constructor.
*
* @var string[]
*/
protected $_propertyNames = [];
/**
* The regular expression used to determine where to split camelCased
* propertyNames into underscored property_names.
*
* @var string
*/
protected $_propertyTranslationRegex = '|([a-z])([A-Z0-9])|';
/**
* The original JSON used to create this Thing object via its fromJson()
* method, if applicable. Typically, this will be the JSON response from
* the Reddit API server. Used by the test suite to ensure that the JSON
* generated by toJson() is functionally equivalent to what Reddit sent us.
*
* @var string
*/
protected $_sourceJson = '';
/**
* The base36 identifier of this Thing, obtained from Reddit. Every Thing
* must have one.
*
* @var string
*/
protected $id = null;
/**
* The unix epoch timestamp at which this Thing was created. Reddit
* supplies this as a float value; the fractional part is always zero.
*
* @var float
*/
protected $created = 0.0;
/**
* The unix epoch UTC timestamp at which this Thing was created. Reddit
* supplies this as a float value; the fractional part is always zero.
*
* @var float
*/
protected $createdUtc = 0.0;
public function __construct(string $kind) {
/* All SnuzeObject subtypes must call parent ctor */
parent::__construct();
/* Set local properties */
$this->setKind($kind);
}
/**
* Get the original JSON used to create this object via fromJson(), if
* applicable. This is intended for use by the test suite. You probably
* want toJson() instead!
*
* @return string If this object was created using fromJson(), the original
* JSON provided; otherwise, an empty string
*/
public function _getSourceJson(): string {
return $this->_sourceJson;
}
/**
* Get the Reddit internal identifier for this object.
*
* @return string|null The Reddit internal identifier for this object
*/
public function getId(): ?string {
return $this->id;
}
/**
* Set the Reddit internal identifier for this object.
*
* @param string $id
* @return $this This Thing object
*/
protected function setId(string $id) {
/* Validate the ID so we don't construct a bogus object */
if (!preg_match(self::REGEX_VALID_ID, $id)) {
throw new \snuze\Exception\ArgumentException($this,
'Invalid Thing id');
}
$this->id = $id;
return $this;
}
public function getKind(): ?string {
return $this->_kind;
}
protected function setKind(string $kind) {
if (!in_array($kind, $this->getValidKinds())) {
throw new \snuze\Exception\ArgumentException($this,
"Unrecognized Thing kind '{$kind}'");
}
$this->_kind = $kind;
}
/**
* Get the epoch time this Thing was created. Reddit provides this as a
* float value, but the fractional part is always zero.
*
* @return float A unix epoch timestamp as a float
*/
public function getCreated(): float {
return $this->created;
}
/**
* Set the epoch time this Thing was created. Reddit provides this as a
* float value, but the fractional part is always zero.
*
* @param float $created A unix epoch timestamp as a float
* @return $this This Thing object
*/
protected function setCreated(float $created) {
$this->created = $created;
return $this;
}
/**
* Get the UTC epoch time this Thing was created. Reddit provides this as
* a float value, but the fractional part is always zero.
*
* @return float A unix epoch timestamp as a float
*/
public function getCreatedUtc(): float {
return $this->createdUtc;
}
/**
* Set the UTC epoch time this Thing was created. Reddit provides this as a
* float value, but the fractional part is always zero.
*
* @param float $createdUtc A unix epoch timestamp as a float
* @return $this This Thing object
*/
protected function setCreatedUtc(float $createdUtc) {
$this->createdUtc = $createdUtc;
return $this;
}
/**
* Get the Reddit fullname for this object. A fullname is comprised of
* the Thing's kind, an underscore, and the Thing's ID, e.g. "t2_bva6"
*
* @return string The Reddit fullname for this object
* @throws \snuze\Exception\RuntimeException
*/
public function getFullname(): string {
if (empty($this->id)) {
throw new \snuze\Exception\RuntimeException($this,
"Can't build a fullname when no ID is set");
}
return $this->_kind . '_' . $this->id;
}
/**
* Return an array whose keys are the class's camelCase property names, and
* whose values are the corresponding under_score names from Reddit's JSON.
* This map is used by the toJson() and fromJson() methods. This is mildly
* more convenient than manually mapping property names in each child class.
*
* All Thing children must set their own $this->_propertyNames variable in
* their constructor.
*
* @return array An array of property names as described above
* @throws \snuze\Exception\RuntimeException
*/
protected function getPropertyTranslationMap(): array {
/* Bail if there's nothing to do */
if (empty($this->_propertyNames)) {
throw new \snuze\Exception\RuntimeException($this,
'No property names are configured! Set '
. '$this->_propertyNames in the ctor for ' . static::class);
}
/* Translate propertyNames to property_names */
$map = [];
foreach ($this->_propertyNames as $camel) {
/*
* Snuze object meta-properties, whose names begin with underscores,
* shouldn't be mapped or exposed
*/
if (strpos($camel, '_') === 0) {
continue;
}
$map[$camel] = preg_replace_callback($this->_propertyTranslationRegex,
function($match) {
return $match[1] . '_' . strtolower($match[2]);
}, $camel);
}
return $map;
}
/**
* Returns a JSON-formatted string representing this Thing's properties.
* Ideally, this should match Reddit's own data structure for returning
* "things" from the API.
*
* @return string
*/
public function toJson(): string {
/* Build an array of underscored property_names and their values */
$data = [];
foreach ($this->getPropertyTranslationMap() as $camel => $underscore) {
$data[$underscore] = $this->$camel;
}
/* Wrap the properties array in Reddit's "thing" JSON structure */
$arr = [
'kind' => $this->getKind(),
'data' => $data
];
return json_encode($arr,
JSON_UNESCAPED_SLASHES | JSON_PRESERVE_ZERO_FRACTION);
}
/**
* Accepts a JSON-formatted string, uses it to build a Thing object, and
* returns that object. This satisfies a promise made in the Jsonable
* interface.
*
* @param string $json
* @return Some subtype of \snuze\Reddit\Thing\Thing
* @see \snuze\Interfaces\Jsonable
*/
public function fromJson(string $json) {
if (func_num_args() !== 1) {
throw new \snuze\Exception\ArgumentException($this,
'fromJson() accepts one argument only');
}
/* Cache the incoming JSON; this may be used by the test suite */
$this->_sourceJson = $json;
/* Turn the incoming JSON into an array for easier manipulation */
if (empty($jsonArray = json_decode($json, true))) {
throw new \snuze\Exception\RuntimeException($this,
'Incoming JSON was empty or ' . "couldn't be decoded.");
}
/* Check that the provided JSON was for the appropriate kind of Thing */
if ($jsonArray['kind'] !== $this->getKind()) {
throw new \snuze\Exception\RuntimeException($this,
'Incoming JSON contains the wrong kind of Thing; expected '
. "{$this->getKind()} but got {$jsonArray['kind']}. "
. 'Possible request for non-existent object?');
}
/* The significant properties of a Thing are in the 'data' element */
if (empty($jsonArray['data'])) {
throw new \snuze\Exception\RuntimeException($this,
"Incoming JSON is missing expected 'data' element");
}
$jsonArray = $jsonArray['data'];
foreach ($this->getPropertyTranslationMap() as $camel => $underscore) {
/* Log and skip any fields that Reddit left null or didn't send */
if (!isset($jsonArray[$underscore])) {
$this->debug("Field {$underscore} null or absent from JSON; skipping");
continue;
}
/*
* If the class has a property corresponding to this field, set it.
* Setter methods are used in case they perform any special handling
* beyond simple assignment.
*/
if (property_exists(static::class, $camel)) {
$fn = 'set' . $camel;
//$this->debug("Calling {$fn}({$jsonArray[$underscore]})");
$this->$fn($jsonArray[$underscore]);
}
}
return $this;
}
/**
* Get an array of supported Thing kinds.
*
* @return array Thing's KIND_ constants and their values
*/
public static function getValidKinds(): array {
return array_filter((new \ReflectionClass(__CLASS__))->getConstants(),
function($value, $key) {
return (strpos($key, 'KIND_') === 0);
}, ARRAY_FILTER_USE_BOTH);
}
}
You can’t perform that action at this time.