588 lines
21 KiB
PHP
588 lines
21 KiB
PHP
<?php
|
|
namespace Misuzu\Forum;
|
|
|
|
use Misuzu\DB;
|
|
use Misuzu\Memoizer;
|
|
use Misuzu\Pagination;
|
|
use Misuzu\Users\User;
|
|
|
|
class ForumTopicException extends ForumException {}
|
|
class ForumTopicNotFoundException extends ForumTopicException {}
|
|
class ForumTopicCreationFailedException extends ForumTopicException {}
|
|
class ForumTopicUpdateFailedException extends ForumTopicException {}
|
|
|
|
class ForumTopic {
|
|
public const TYPE_DISCUSSION = 0;
|
|
public const TYPE_STICKY = 1;
|
|
public const TYPE_ANNOUNCEMENT = 2;
|
|
public const TYPE_GLOBAL_ANNOUNCEMENT = 3;
|
|
|
|
public const TYPES = [
|
|
self::TYPE_DISCUSSION,
|
|
self::TYPE_STICKY,
|
|
self::TYPE_ANNOUNCEMENT,
|
|
self::TYPE_GLOBAL_ANNOUNCEMENT,
|
|
];
|
|
|
|
public const TYPE_ORDER = [
|
|
self::TYPE_GLOBAL_ANNOUNCEMENT,
|
|
self::TYPE_ANNOUNCEMENT,
|
|
self::TYPE_STICKY,
|
|
self::TYPE_DISCUSSION,
|
|
];
|
|
|
|
public const TYPE_IMPORTANT = [
|
|
self::TYPE_STICKY,
|
|
self::TYPE_ANNOUNCEMENT,
|
|
self::TYPE_GLOBAL_ANNOUNCEMENT,
|
|
];
|
|
|
|
public const TITLE_MIN_LENGTH = 3;
|
|
public const TITLE_MAX_LENGTH = 100;
|
|
|
|
public const DELETE_AGE_LIMIT = 60 * 60 * 24;
|
|
public const DELETE_POST_LIMIT = 2;
|
|
|
|
public const UNREAD_TIME_LIMIT = 60 * 60 * 24 * 31;
|
|
|
|
// Database fields
|
|
private $topic_id = -1;
|
|
private $forum_id = -1;
|
|
private $user_id = null;
|
|
private $topic_type = self::TYPE_DISCUSSION;
|
|
private $topic_title = '';
|
|
private $topic_priority = 0;
|
|
private $topic_count_posts = 0;
|
|
private $topic_count_views = 0;
|
|
private $topic_post_first = null;
|
|
private $topic_post_last = null;
|
|
private $topic_created = null;
|
|
private $topic_bumped = null;
|
|
private $topic_deleted = null;
|
|
private $topic_locked = null;
|
|
|
|
public const TABLE = 'forum_topics';
|
|
private const QUERY_SELECT = 'SELECT %1$s FROM `' . DB::PREFIX . self::TABLE . '` AS '. self::TABLE;
|
|
private const SELECT = '%1$s.`topic_id`, %1$s.`forum_id`, %1$s.`user_id`, %1$s.`topic_type`, %1$s.`topic_title`'
|
|
. ', %1$s.`topic_count_posts`, %1$s.`topic_count_views`, %1$s.`topic_post_first`, %1$s.`topic_post_last`'
|
|
. ', UNIX_TIMESTAMP(%1$s.`topic_created`) AS `topic_created`'
|
|
. ', UNIX_TIMESTAMP(%1$s.`topic_bumped`) AS `topic_bumped`'
|
|
. ', UNIX_TIMESTAMP(%1$s.`topic_deleted`) AS `topic_deleted`'
|
|
. ', UNIX_TIMESTAMP(%1$s.`topic_locked`) AS `topic_locked`';
|
|
|
|
private $category = null;
|
|
private $user = null;
|
|
private $firstPost = -1;
|
|
private $lastPost = -1;
|
|
private $priorityVotes = null;
|
|
private $polls = [];
|
|
|
|
public function getId(): int {
|
|
return $this->topic_id < 1 ? -1 : $this->topic_id;
|
|
}
|
|
|
|
public function getCategoryId(): int {
|
|
return $this->forum_id < 1 ? -1 : $this->forum_id;
|
|
}
|
|
public function setCategoryId(int $categoryId): self {
|
|
$this->forum_id = $categoryId;
|
|
$this->category = null;
|
|
return $this;
|
|
}
|
|
public function getCategory(): ForumCategory {
|
|
if($this->category === null)
|
|
$this->category = ForumCategory::byId($this->getCategoryId());
|
|
return $this->category;
|
|
}
|
|
public function setCategory(ForumCategory $category): self {
|
|
$this->forum_id = $category->getId();
|
|
$this->category = $category;
|
|
return $this;
|
|
}
|
|
|
|
public function getUserId(): int {
|
|
return $this->user_id < 1 ? -1 : $this->user_id;
|
|
}
|
|
public function setUserId(?int $userId): self {
|
|
$this->user_id = $userId < 1 ? null : $userId;
|
|
$this->user = null;
|
|
return $this;
|
|
}
|
|
public function getUser(): ?User {
|
|
if($this->user === null && ($userId = $this->getUserId()) > 0)
|
|
$this->user = User::byId($userId);
|
|
return $this->user;
|
|
}
|
|
public function hasUser(): bool {
|
|
return $this->getUserId() > 0;
|
|
}
|
|
public function setUser(?User $user): self {
|
|
$this->user_id = $user === null ? null : $user->getId();
|
|
$this->user = $user;
|
|
return $this;
|
|
}
|
|
|
|
public function getType(): int {
|
|
return $this->topic_type;
|
|
}
|
|
public function setType(int $type): self {
|
|
$this->topic_type = $type;
|
|
return $this;
|
|
}
|
|
public function isNormal(): bool { return $this->getType() === self::TYPE_DISCUSSION; }
|
|
public function isSticky(): bool { return $this->getType() === self::TYPE_STICKY; }
|
|
public function isAnnouncement(): bool { return $this->getType() === self::TYPE_ANNOUNCEMENT; }
|
|
public function isGlobalAnnouncement(): bool { return $this->getType() === self::TYPE_GLOBAL_ANNOUNCEMENT; }
|
|
|
|
public function isImportant(): bool {
|
|
return in_array($this->getType(), self::TYPE_IMPORTANT);
|
|
}
|
|
|
|
public function hasPriorityVoting(): bool {
|
|
return $this->getCategory()->canHavePriorityVotes();
|
|
}
|
|
|
|
public function getIcon(?User $viewer = null): string {
|
|
if($this->isDeleted())
|
|
return 'fas fa-trash-alt fa-fw';
|
|
|
|
if($this->isGlobalAnnouncement() || $this->isAnnouncement())
|
|
return 'fas fa-bullhorn fa-fw';
|
|
if($this->isSticky())
|
|
return 'fas fa-thumbtack fa-fw';
|
|
|
|
if($this->isLocked())
|
|
return 'fas fa-lock fa-fw';
|
|
|
|
if($this->hasPriorityVoting())
|
|
return 'far fa-star fa-fw';
|
|
|
|
return ($viewer === null || $this->hasRead($viewer) ? 'far' : 'fas') . ' fa-comment fa-fw';
|
|
}
|
|
|
|
public function getTitle(): string {
|
|
return $this->topic_title ?? '';
|
|
}
|
|
public function setTitle(string $title): self {
|
|
$this->topic_title = $title;
|
|
return $this;
|
|
}
|
|
|
|
public function getPriority(): int {
|
|
return $this->topic_priority < 1 ? 0 : $this->topic_priority;
|
|
}
|
|
|
|
public function getPostCount(): int {
|
|
return $this->topic_count_posts;
|
|
}
|
|
public function getPageCount(int $postsPerPage = 10): int {
|
|
return ceil($this->getPostCount() / $postsPerPage);
|
|
}
|
|
|
|
public function getViewCount(): int {
|
|
return $this->topic_count_views;
|
|
}
|
|
public function incrementViewCount(): void {
|
|
++$this->topic_count_views;
|
|
DB::prepare('UPDATE `msz_forum_topics` SET `topic_count_views` = `topic_count_views` + 1 WHERE `topic_id` = :topic')
|
|
->bind('topic', $this->getId())
|
|
->execute();
|
|
}
|
|
|
|
public function getFirstPostId(): int {
|
|
return $this->topic_post_first < 1 ? -1 : $this->topic_post_first;
|
|
}
|
|
public function hasFirstPost(): bool {
|
|
return $this->getFirstPostId() > 0;
|
|
}
|
|
public function getFirstPost(): ?ForumPost {
|
|
if($this->firstPost === -1) {
|
|
if(!$this->hasFirstPost())
|
|
return null;
|
|
try {
|
|
$this->firstPost = ForumPost::byId($this->getFirstPostId());
|
|
} catch(ForumPostNotFoundException $ex) {
|
|
$this->firstPost = null;
|
|
}
|
|
}
|
|
return $this->firstPost;
|
|
}
|
|
|
|
public function getLastPostId(): int {
|
|
return $this->topic_post_last < 1 ? -1 : $this->topic_post_last;
|
|
}
|
|
public function hasLastPost(): bool {
|
|
return $this->getLastPostId() > 0;
|
|
}
|
|
public function getLastPost(): ?ForumPost {
|
|
if($this->lastPost === -1) {
|
|
if(!$this->hasLastPost())
|
|
return null;
|
|
try {
|
|
$this->lastPost = ForumPost::byId($this->getLastPostId());
|
|
} catch(ForumPostNotFoundException $ex) {
|
|
$this->lastPost = null;
|
|
}
|
|
}
|
|
return $this->lastPost;
|
|
}
|
|
|
|
public function getCreatedTime(): int {
|
|
return $this->topic_created === null ? -1 : $this->topic_created;
|
|
}
|
|
|
|
public function getBumpedTime(): int {
|
|
return $this->topic_bumped === null ? -1 : $this->topic_bumped;
|
|
}
|
|
public function bumpTopic(): void {
|
|
if($this->isDeleted())
|
|
return;
|
|
$this->topic_bumped = time();
|
|
DB::prepare(
|
|
'UPDATE `' . DB::PREFIX . self::TABLE . '`'
|
|
. ' SET `topic_bumped` = NOW()'
|
|
. ' WHERE `topic_id` = :topic'
|
|
. ' AND `topic_deleted` IS NULL'
|
|
)->bind('topic', $this->getId())->execute();
|
|
}
|
|
|
|
public function getDeletedTime(): int {
|
|
return $this->topic_deleted === null ? -1 : $this->topic_deleted;
|
|
}
|
|
public function isDeleted(): bool {
|
|
return $this->getDeletedTime() >= 0;
|
|
}
|
|
|
|
public function getLockedTime(): int {
|
|
return $this->topic_locked === null ? -1 : $this->topic_locked;
|
|
}
|
|
public function isLocked(): bool {
|
|
return $this->getLockedTime() >= 0;
|
|
}
|
|
public function setLocked(bool $locked): self {
|
|
if($this->isLocked() !== $locked)
|
|
$this->topic_locked = $locked ? time() : null;
|
|
return $this;
|
|
}
|
|
|
|
public function isArchived(): bool {
|
|
return $this->getCategory()->isArchived();
|
|
}
|
|
|
|
public function getActualPostCount(bool $includeDeleted = false): int {
|
|
return ForumPost::countByTopic($this, $includeDeleted);
|
|
}
|
|
public function getPosts(bool $includeDeleted = false, ?Pagination $pagination = null): array {
|
|
return ForumPost::byTopic($this, $includeDeleted, $pagination);
|
|
}
|
|
|
|
public function getPolls(): array {
|
|
if($this->polls === null)
|
|
$this->polls = ForumPoll::byTopic($this);
|
|
return $this->polls;
|
|
}
|
|
|
|
public function isAbandoned(): bool {
|
|
return $this->getBumpedTime() < time() - self::UNREAD_TIME_LIMIT;
|
|
}
|
|
public function hasRead(User $user): bool {
|
|
if($this->isAbandoned())
|
|
return true;
|
|
|
|
try {
|
|
$trackInfo = ForumTopicTrack::byTopicAndUser($this, $user);
|
|
return $trackInfo->getReadTime() >= $this->getBumpedTime();
|
|
} catch(ForumTopicTrackNotFoundException $ex) {
|
|
return false;
|
|
}
|
|
}
|
|
public function markRead(User $user): void {
|
|
if(!$this->hasRead($user))
|
|
$this->incrementViewCount();
|
|
ForumTopicTrack::bump($this, $user);
|
|
}
|
|
|
|
public function hasParticipated(?User $user): bool {
|
|
return $user !== null;
|
|
}
|
|
|
|
public function isOpeningPost(ForumPost $post): bool {
|
|
$firstPost = $this->getFirstPost();
|
|
return $firstPost !== null && $firstPost->getId() === $post->getId();
|
|
}
|
|
public function isTopicAuthor(?User $user): bool {
|
|
if($user === null)
|
|
return false;
|
|
return $user->getId() === $this->getUser()->getId();
|
|
}
|
|
|
|
public function getPriorityVotes(): array {
|
|
if($this->priorityVotes === null)
|
|
$this->priorityVotes = ForumTopicPriority::byTopic($this);
|
|
return $this->priorityVotes;
|
|
}
|
|
|
|
public function canVoteOnPriority(?User $user): bool {
|
|
if($user === null || !$this->hasPriorityVoting())
|
|
return false;
|
|
// shouldn't there be an actual permission for this?
|
|
return $this->getCategory()->canView($user);
|
|
}
|
|
|
|
public function canBeDeleted(User $user): string {
|
|
if(false) // check if viewable
|
|
return 'view';
|
|
|
|
// check if user can view deleted posts/is mod
|
|
$canDeleteAny = false;
|
|
|
|
if($this->isDeleted())
|
|
return $canDeleteAny ? 'deleted' : 'view';
|
|
|
|
if(!$canDeleteAny) {
|
|
if(false) // check if user can delete posts
|
|
return 'permission';
|
|
if($user->getId() !== $this->getUserId())
|
|
return 'owner';
|
|
if($this->getCreatedTime() <= time() - self::DELETE_AGE_LIMIT)
|
|
return 'age';
|
|
if($this->getActualPostCount(true) >= self::DELETE_POST_LIMIT)
|
|
return 'posts';
|
|
}
|
|
|
|
return '';
|
|
}
|
|
public static function canBeDeletedErrorString(string $error): string {
|
|
switch($error) {
|
|
case 'view':
|
|
return 'This topic doesn\'t exist.';
|
|
case 'deleted':
|
|
return 'This topic has already been marked as deleted.';
|
|
case 'permission':
|
|
return 'You aren\'t allowed to this topic.';
|
|
case 'owner':
|
|
return 'You can only delete your own topics.';
|
|
case 'age':
|
|
return 'This topic is too old to be deleted. Ask a moderator to remove it if you deem it absolutely necessary.';
|
|
case 'posts':
|
|
return 'This topic has too many replies to be deleted. Ask a moderator to remove it if you deem it absolutely necessary.';
|
|
case '':
|
|
return 'Topic can be deleted!';
|
|
default:
|
|
return 'Topic cannot be deleted.';
|
|
}
|
|
}
|
|
|
|
public function delete(): void {
|
|
if($this->isDeleted())
|
|
return;
|
|
$this->topic_deleted = time();
|
|
DB::prepare('UPDATE `' . DB::PREFIX . self::TABLE . '` SET `topic_deleted` = NOW() WHERE `topic_id` = :topic')
|
|
->bind('topic', $this->getId())
|
|
->execute();
|
|
ForumPost::deleteTopic($this);
|
|
}
|
|
public function restore(): void {
|
|
if(!$this->isDeleted())
|
|
return;
|
|
ForumPost::restoreTopic($this);
|
|
DB::prepare('UPDATE `' . DB::PREFIX . self::TABLE . '` SET `topic_deleted` = NULL WHERE `topic_id` = :topic')
|
|
->bind('topic', $this->getId())
|
|
->execute();
|
|
$this->topic_deleted = null;
|
|
}
|
|
public function nuke(): void {
|
|
if(!$this->isDeleted())
|
|
return;
|
|
DB::prepare('DELETE FROM `' . DB::PREFIX . self::TABLE . '` WHERE `topic_id` = :topic')
|
|
->bind('topic', $this->getId())
|
|
->execute();
|
|
//ForumPost::nukeTopic($this);
|
|
}
|
|
|
|
public static function create(ForumCategory $category, User $user, string $title, int $type = self::TYPE_DISCUSSION): ForumTopic {
|
|
$create = DB::prepare(
|
|
'INSERT INTO `msz_forum_topics` (`forum_id`, `user_id`, `topic_title`, `topic_type`) VALUES (:forum, :user, :title, :type)'
|
|
)->bind('forum', $category->getId())->bind('user', $user->getId())
|
|
->bind('title', $title)->bind('type', $type)
|
|
->execute();
|
|
if(!$create)
|
|
throw new ForumTopicCreationFailedException;
|
|
$topicId = DB::lastId();
|
|
if($topicId < 1)
|
|
throw new ForumTopicCreationFailedException;
|
|
|
|
try {
|
|
return self::byId($topicId);
|
|
} catch(ForumTopicNotFoundException $ex) {
|
|
throw new ForumTopicCreationFailedException;
|
|
}
|
|
}
|
|
|
|
public function update(): void {
|
|
if($this->getId() < 1)
|
|
throw new ForumTopicUpdateFailedException;
|
|
|
|
if(!DB::prepare(
|
|
'UPDATE `' . DB::PREFIX . self::TABLE . '`'
|
|
. ' SET `topic_title` = :title,'
|
|
. ' `topic_type` = :type'
|
|
. ' WHERE `topic_id` = :topic'
|
|
)->bind('topic', $this->getId())
|
|
->bind('title', $this->getTitle())
|
|
->bind('type', $this->getType())
|
|
->execute())
|
|
throw new ForumTopicUpdateFailedException;
|
|
}
|
|
|
|
public function synchronise(bool $save = true): array {
|
|
$stats = DB::prepare(
|
|
'SELECT :topic AS `topic`, ('
|
|
. 'SELECT MIN(`post_id`) FROM `msz_forum_posts` WHERE `topic_id` = `topic`' // this shouldn't be deleteable without nuking the topic
|
|
. ') AS `first_post`, ('
|
|
. 'SELECT MAX(`post_id`) FROM `msz_forum_posts` WHERE `topic_id` = `topic` AND `post_deleted` IS NULL'
|
|
. ') AS `last_post`, ('
|
|
. 'SELECT COUNT(*) FROM `msz_forum_posts` WHERE `topic_id` = `topic` AND `post_deleted` IS NULL'
|
|
. ') AS `posts`, ('
|
|
. 'SELECT UNIX_TIMESTAMP(`post_created`) FROM `msz_forum_posts` WHERE `post_id` = `last_post`'
|
|
. ') AS `last_post_time`'
|
|
)->bind('topic', $this->getId())->fetch();
|
|
|
|
if($save) {
|
|
$this->topic_post_first = $stats['first_post'];
|
|
$this->topic_post_last = $stats['last_post'];
|
|
$this->topic_count_posts = $stats['posts'];
|
|
DB::prepare(
|
|
'UPDATE `msz_forum_topics`'
|
|
. ' SET `topic_post_first` = :first'
|
|
. ', `topic_post_last` = :last'
|
|
. ', `topic_count_posts` = :posts'
|
|
. ' WHERE `topic_id` = :topic'
|
|
) ->bind('first', $this->topic_post_first)
|
|
->bind('last', $this->topic_post_last)
|
|
->bind('posts', $this->topic_count_posts)
|
|
->bind('topic', $this->getId())
|
|
->execute();
|
|
}
|
|
|
|
return $stats;
|
|
}
|
|
|
|
public static function validateTitle(string $title): string {
|
|
$length = mb_strlen(trim($title));
|
|
if($length < self::TITLE_MIN_LENGTH)
|
|
return 'short';
|
|
if($length > self::TITLE_MAX_LENGTH)
|
|
return 'long';
|
|
return '';
|
|
}
|
|
public static function titleValidationErrorString(string $error): string {
|
|
switch($error) {
|
|
case 'short':
|
|
return sprintf('Topic title was too short, it has to be at least %d characters!', self::TITLE_MIN_LENGTH);
|
|
case 'long':
|
|
return sprintf("Topic title was too long, it can't be longer than %d characters!", self::TITLE_MAX_LENGTH);
|
|
case '':
|
|
return 'Topic title is correctly formatted!';
|
|
default:
|
|
return 'Topic title is incorrectly formatted.';
|
|
}
|
|
}
|
|
|
|
private static function countQueryBase(): string {
|
|
return sprintf(self::QUERY_SELECT, sprintf('COUNT(*)', self::TABLE));
|
|
}
|
|
public static function countByCategory(ForumCategory $category, bool $includeDeleted = false): int {
|
|
return (int)DB::prepare(
|
|
self::countQueryBase()
|
|
. ' WHERE `forum_id` = :category'
|
|
. ($includeDeleted ? '' : ' AND `topic_deleted` IS NULL')
|
|
)->bind('category', $category->getId())->fetchColumn();
|
|
}
|
|
|
|
private static function memoizer() {
|
|
static $memoizer = null;
|
|
if($memoizer === null)
|
|
$memoizer = new Memoizer;
|
|
return $memoizer;
|
|
}
|
|
|
|
private static function byQueryBase(): string {
|
|
return sprintf(self::QUERY_SELECT, sprintf(self::SELECT, self::TABLE));
|
|
}
|
|
public static function byId(int $topicId): self {
|
|
return self::memoizer()->find($topicId, function() use ($topicId) {
|
|
$object = DB::prepare(self::byQueryBase() . ' WHERE `topic_id` = :topic')
|
|
->bind('topic', $topicId)
|
|
->fetchObject(self::class);
|
|
if(!$object)
|
|
throw new ForumTopicNotFoundException;
|
|
return $object;
|
|
});
|
|
}
|
|
public static function byCategoryLast(ForumCategory $category): ?self {
|
|
return self::memoizer()->find(function($topic) use ($category) {
|
|
// This doesn't actually do what is advertised, but should be fine for the time being.
|
|
return $topic->getCategory()->getId() === $category->getId() && !$topic->isDeleted();
|
|
}, function() use ($category) {
|
|
return DB::prepare(
|
|
self::byQueryBase()
|
|
. ' WHERE `forum_id` = :category AND `topic_deleted` IS NULL'
|
|
. ' ORDER BY `topic_bumped` DESC'
|
|
. ' LIMIT 1'
|
|
)->bind('category', $category->getId())->fetchObject(self::class);
|
|
});
|
|
}
|
|
public static function byCategory(ForumCategory $category, bool $includeDeleted = false, ?Pagination $pagination = null): array {
|
|
if(!$category->canHaveTopics())
|
|
return [];
|
|
|
|
$query = self::byQueryBase()
|
|
. ' WHERE `forum_id` = :category'
|
|
. ($includeDeleted ? '' : ' AND `topic_deleted` IS NULL')
|
|
. ' ORDER BY FIELD(`topic_type`, ' . implode(',', self::TYPE_ORDER) . ')';
|
|
|
|
//if($category->canHavePriorityVotes())
|
|
// $query .= ', `topic_priority` DESC';
|
|
|
|
$query .= ', `topic_bumped` DESC';
|
|
|
|
if($pagination !== null)
|
|
$query .= ' LIMIT :range OFFSET :offset';
|
|
|
|
$getObjects = DB::prepare($query)
|
|
->bind('category', $category->getId());
|
|
|
|
if($pagination !== null)
|
|
$getObjects->bind('range', $pagination->getRange())
|
|
->bind('offset', $pagination->getOffset());
|
|
|
|
$objects = [];
|
|
$memoizer = self::memoizer();
|
|
while($object = $getObjects->fetchObject(self::class))
|
|
$memoizer->insert($objects[] = $object);
|
|
return $objects;
|
|
}
|
|
public static function bySearchQuery(string $search, bool $includeDeleted = false, ?Pagination $pagination = null): array {
|
|
$query = self::byQueryBase()
|
|
. ' WHERE MATCH(`topic_title`) AGAINST (:search IN NATURAL LANGUAGE MODE)'
|
|
. ($includeDeleted ? '' : ' AND `topic_deleted` IS NULL')
|
|
. ' ORDER BY FIELD(`topic_type`, ' . implode(',', self::TYPE_ORDER) . '), `topic_bumped` DESC';
|
|
|
|
if($pagination !== null)
|
|
$query .= ' LIMIT :range OFFSET :offset';
|
|
|
|
$getObjects = DB::prepare($query)
|
|
->bind('search', $search);
|
|
|
|
if($pagination !== null)
|
|
$getObjects->bind('range', $pagination->getRange())
|
|
->bind('offset', $pagination->getOffset());
|
|
|
|
$objects = [];
|
|
$memoizer = self::memoizer();
|
|
while($object = $getObjects->fetchObject(self::class))
|
|
$memoizer->insert($objects[] = $object);
|
|
return $objects;
|
|
}
|
|
}
|