Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
52.17% covered (warning)
52.17%
24 / 46
33.33% covered (danger)
33.33%
2 / 6
CRAP
0.00% covered (danger)
0.00%
0 / 1
LocalCacheAbstractCollection
52.17% covered (warning)
52.17%
24 / 46
33.33% covered (danger)
33.33%
2 / 6
58.49
0.00% covered (danger)
0.00%
0 / 1
 getAllRevisions
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
2
 getRevision
73.33% covered (warning)
73.33%
11 / 15
0.00% covered (danger)
0.00%
0 / 1
7.93
 getLastRevision
37.50% covered (danger)
37.50%
6 / 16
0.00% covered (danger)
0.00%
0 / 1
7.91
 getNextRevision
0.00% covered (danger)
0.00%
0 / 7
0.00% covered (danger)
0.00%
0 / 1
6
 loaded
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
2
 getOldestLoaded
66.67% covered (warning)
66.67%
2 / 3
0.00% covered (danger)
0.00%
0 / 1
2.15
1<?php
2
3namespace Flow\Collection;
4
5use Flow\Exception\InvalidDataException;
6use Flow\Model\AbstractRevision;
7use Flow\Model\UUID;
8
9/**
10 * LocalBufferedCache saves all data that has been requested in an internal
11 * cache (in memory, per request). This provides the opportunity of (trying to)
12 * be smart about what results we fetch.
13 * The class extends the default AbstractCollection to make sure not all
14 * revisions are loaded unless we really need them. It could very well be that
15 * perhaps 5 recent revisions have already been loaded in other parts of the
16 * code, and we only need the 3rd most recent, in which case we shouldn't
17 * try to fetch all of them.
18 */
19abstract class LocalCacheAbstractCollection extends AbstractCollection {
20    /**
21     * Returns all revisions.
22     *
23     * @return AbstractRevision[]
24     */
25    public function getAllRevisions() {
26        // if we have not yet loaded everything, just clear what we have and
27        // fetch from cache
28        if ( !$this->loaded() ) {
29            $this->revisions = [];
30        }
31
32        return parent::getAllRevisions();
33    }
34
35    /**
36     * Returns the revision with the given id.
37     *
38     * @param UUID $uuid
39     * @return AbstractRevision|null null if there is no such revision
40     */
41    public function getRevision( UUID $uuid ) {
42        // check if fetching last already res
43        if ( isset( $this->revisions[$uuid->getAlphadecimal() ] ) ) {
44            return $this->revisions[$uuid->getAlphadecimal() ];
45        }
46
47        /*
48         * The strategy here is to avoid having to call getAllRevisions(), which
49         * is most likely to have to load (fresh) data that is not yet in
50         * LocalBufferedCache's internal cache.
51         * To do so, we'll build the $this->revisions array by hand. Starting at
52         * the most recent revision and going up 1 revision at a time, checking
53         * if it is already in LocalBufferedCache's cache.
54         * If, however, we can't find the requested revisions (or one of the
55         * revisions on our way to the requested revision) in the internal cache
56         * of LocalBufferedCache, we'll just bail and load all revisions after
57         * all: if we do have to fetch data, might as well do it all in 1 go!
58         */
59        while ( !$this->loaded() ) {
60            // fetch current oldest revision
61            $oldest = $this->getOldestLoaded();
62
63            // fetch that one's preceding revision id
64            $previousId = $oldest->getPrevRevisionId();
65
66            // check if it's in local storage already
67            if ( $previousId && self::getStorage()->got( $previousId ) ) {
68                $revision = self::getStorage()->get( $previousId );
69
70                // add this revision to revisions array
71                $this->revisions[$previousId->getAlphadecimal()] = $revision;
72
73                // stop iterating if we've found the one we wanted
74                if ( $uuid->equals( $previousId ) ) {
75                    break;
76                }
77            } else {
78                // revision not found in local storage: load all revisions
79                $this->getAllRevisions();
80                break;
81            }
82        }
83
84        if ( !isset( $this->revisions[$uuid->getAlphadecimal()] ) ) {
85            return null;
86        }
87
88        return $this->revisions[$uuid->getAlphadecimal()];
89    }
90
91    /**
92     * Returns the most recent revision.
93     *
94     * @return AbstractRevision
95     * @throws InvalidDataException When no revision can be located
96     */
97    public function getLastRevision() {
98        // if $revisions is not empty, it will always have the last revision,
99        // at the beginning of the array
100        if ( $this->revisions ) {
101            return reset( $this->revisions );
102        }
103
104        $attributes = [ 'rev_type_id' => $this->uuid ];
105        $options = [ 'sort' => 'rev_id', 'limit' => 1, 'order' => 'DESC' ];
106
107        if ( self::getStorage()->found( $attributes, $options ) ) {
108            // if last revision is already known in local cache, fetch it
109            $revision = self::getStorage()->find( $attributes, $options );
110            if ( !$revision ) {
111                throw new InvalidDataException(
112                    'Last revision for ' . $this->uuid->getAlphadecimal() . ' could not be found',
113                    'invalid-type-id'
114                );
115            }
116            $revision = reset( $revision );
117            $this->revisions[$revision->getRevisionId()->getAlphadecimal()] = $revision;
118            return $revision;
119
120        } else {
121            // otherwise, might as well fetch all previous revisions while we're at
122            // it - saves roundtrips to cache/db
123            $this->getAllRevisions();
124            return reset( $this->revisions );
125        }
126    }
127
128    /**
129     * Given a certain revision, returns the next revision.
130     *
131     * @param AbstractRevision $revision
132     * @return AbstractRevision|null null if there is no next revision
133     */
134    public function getNextRevision( AbstractRevision $revision ) {
135        // make sure the given revision is loaded
136        $this->getRevision( $revision->getRevisionId() );
137
138        // find requested id, based on given revision
139        $ids = array_keys( $this->revisions );
140        $current = array_search( $revision->getRevisionId()->getAlphadecimal(), $ids );
141        $next = $current - 1;
142
143        if ( $next < 0 ) {
144            return null;
145        }
146
147        return $this->getRevision( UUID::create( $ids[$next] ) );
148    }
149
150    /**
151     * Returns true if all revisions have been loaded into $this->revisions.
152     *
153     * @return bool
154     */
155    public function loaded() {
156        $first = end( $this->revisions );
157        return $first && $first->getPrevRevisionId() === null;
158    }
159
160    /**
161     * Returns the oldest revision that has already been fetched via this class.
162     *
163     * @return AbstractRevision
164     */
165    public function getOldestLoaded() {
166        if ( !$this->revisions ) {
167            return $this->getLastRevision();
168        }
169
170        return end( $this->revisions );
171    }
172}