--- /dev/null
+<?php
+
+namespace Shaarli\Bookmark;
+
+use Shaarli\Bookmark\Exception\InvalidBookmarkException;
+
+/**
+ * Class BookmarkArray
+ *
+ * Implementing ArrayAccess, this allows us to use the bookmark list
+ * as an array and iterate over it.
+ *
+ * @package Shaarli\Bookmark
+ */
+class BookmarkArray implements \Iterator, \Countable, \ArrayAccess
+{
+ /**
+ * @var Bookmark[]
+ */
+ protected $bookmarks;
+
+ /**
+ * @var array List of all bookmarks IDS mapped with their array offset.
+ * Map: id->offset.
+ */
+ protected $ids;
+
+ /**
+ * @var int Position in the $this->keys array (for the Iterator interface)
+ */
+ protected $position;
+
+ /**
+ * @var array List of offset keys (for the Iterator interface implementation)
+ */
+ protected $keys;
+
+ /**
+ * @var array List of all recorded URLs (key=url, value=bookmark offset)
+ * for fast reserve search (url-->bookmark offset)
+ */
+ protected $urls;
+
+ public function __construct()
+ {
+ $this->ids = [];
+ $this->bookmarks = [];
+ $this->keys = [];
+ $this->urls = [];
+ $this->position = 0;
+ }
+
+ /**
+ * Countable - Counts elements of an object
+ *
+ * @return int Number of bookmarks
+ */
+ public function count()
+ {
+ return count($this->bookmarks);
+ }
+
+ /**
+ * ArrayAccess - Assigns a value to the specified offset
+ *
+ * @param int $offset Bookmark ID
+ * @param Bookmark $value instance
+ *
+ * @throws InvalidBookmarkException
+ */
+ public function offsetSet($offset, $value)
+ {
+ if (! $value instanceof Bookmark
+ || $value->getId() === null || empty($value->getUrl())
+ || ($offset !== null && ! is_int($offset)) || ! is_int($value->getId())
+ || $offset !== null && $offset !== $value->getId()
+ ) {
+ throw new InvalidBookmarkException($value);
+ }
+
+ // If the bookmark exists, we reuse the real offset, otherwise new entry
+ if ($offset !== null) {
+ $existing = $this->getBookmarkOffset($offset);
+ } else {
+ $existing = $this->getBookmarkOffset($value->getId());
+ }
+
+ if ($existing !== null) {
+ $offset = $existing;
+ } else {
+ $offset = count($this->bookmarks);
+ }
+
+ $this->bookmarks[$offset] = $value;
+ $this->urls[$value->getUrl()] = $offset;
+ $this->ids[$value->getId()] = $offset;
+ }
+
+ /**
+ * ArrayAccess - Whether or not an offset exists
+ *
+ * @param int $offset Bookmark ID
+ *
+ * @return bool true if it exists, false otherwise
+ */
+ public function offsetExists($offset)
+ {
+ return array_key_exists($this->getBookmarkOffset($offset), $this->bookmarks);
+ }
+
+ /**
+ * ArrayAccess - Unsets an offset
+ *
+ * @param int $offset Bookmark ID
+ */
+ public function offsetUnset($offset)
+ {
+ $realOffset = $this->getBookmarkOffset($offset);
+ $url = $this->bookmarks[$realOffset]->getUrl();
+ unset($this->urls[$url]);
+ unset($this->ids[$offset]);
+ unset($this->bookmarks[$realOffset]);
+ }
+
+ /**
+ * ArrayAccess - Returns the value at specified offset
+ *
+ * @param int $offset Bookmark ID
+ *
+ * @return Bookmark|null The Bookmark if found, null otherwise
+ */
+ public function offsetGet($offset)
+ {
+ $realOffset = $this->getBookmarkOffset($offset);
+ return isset($this->bookmarks[$realOffset]) ? $this->bookmarks[$realOffset] : null;
+ }
+
+ /**
+ * Iterator - Returns the current element
+ *
+ * @return Bookmark corresponding to the current position
+ */
+ public function current()
+ {
+ return $this[$this->keys[$this->position]];
+ }
+
+ /**
+ * Iterator - Returns the key of the current element
+ *
+ * @return int Bookmark ID corresponding to the current position
+ */
+ public function key()
+ {
+ return $this->keys[$this->position];
+ }
+
+ /**
+ * Iterator - Moves forward to next element
+ */
+ public function next()
+ {
+ ++$this->position;
+ }
+
+ /**
+ * Iterator - Rewinds the Iterator to the first element
+ *
+ * Entries are sorted by date (latest first)
+ */
+ public function rewind()
+ {
+ $this->keys = array_keys($this->ids);
+ $this->position = 0;
+ }
+
+ /**
+ * Iterator - Checks if current position is valid
+ *
+ * @return bool true if the current Bookmark ID exists, false otherwise
+ */
+ public function valid()
+ {
+ return isset($this->keys[$this->position]);
+ }
+
+ /**
+ * Returns a bookmark offset in bookmarks array from its unique ID.
+ *
+ * @param int $id Persistent ID of a bookmark.
+ *
+ * @return int Real offset in local array, or null if doesn't exist.
+ */
+ protected function getBookmarkOffset($id)
+ {
+ if (isset($this->ids[$id])) {
+ return $this->ids[$id];
+ }
+ return null;
+ }
+
+ /**
+ * Return the next key for bookmark creation.
+ * E.g. If the last ID is 597, the next will be 598.
+ *
+ * @return int next ID.
+ */
+ public function getNextId()
+ {
+ if (!empty($this->ids)) {
+ return max(array_keys($this->ids)) + 1;
+ }
+ return 0;
+ }
+
+ /**
+ * @param $url
+ *
+ * @return Bookmark|null
+ */
+ public function getByUrl($url)
+ {
+ if (! empty($url)
+ && isset($this->urls[$url])
+ && isset($this->bookmarks[$this->urls[$url]])
+ ) {
+ return $this->bookmarks[$this->urls[$url]];
+ }
+ return null;
+ }
+
+ /**
+ * Reorder links by creation date (newest first).
+ *
+ * Also update the urls and ids mapping arrays.
+ *
+ * @param string $order ASC|DESC
+ */
+ public function reorder($order = 'DESC')
+ {
+ $order = $order === 'ASC' ? -1 : 1;
+ // Reorder array by dates.
+ usort($this->bookmarks, function ($a, $b) use ($order) {
+ /** @var $a Bookmark */
+ /** @var $b Bookmark */
+ if ($a->isSticky() !== $b->isSticky()) {
+ return $a->isSticky() ? -1 : 1;
+ }
+ return $a->getCreated() < $b->getCreated() ? 1 * $order : -1 * $order;
+ });
+
+ $this->urls = [];
+ $this->ids = [];
+ foreach ($this->bookmarks as $key => $bookmark) {
+ $this->urls[$bookmark->getUrl()] = $key;
+ $this->ids[$bookmark->getId()] = $key;
+ }
+ }
+}