onAfterUnpublish()   A
last analyzed

Complexity

Conditions 2
Paths 2

Size

Total Lines 14
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 2
eloc 5
c 1
b 0
f 0
nc 2
nop 0
dl 0
loc 14
rs 10
1
<?php
2
3
namespace Ichaber\SSSwiftype\Extensions;
4
5
use Ichaber\SSSwiftype\Service\SwiftypeCrawler;
6
use SilverStripe\CMS\Model\SiteTree;
7
use SilverStripe\CMS\Model\SiteTreeExtension;
8
use SilverStripe\Control\Director;
9
use SilverStripe\Core\Config\Config;
10
use SilverStripe\Versioned\Versioned;
11
12
/**
13
 * Class SwiftypeSiteTreeCrawlerExtension
14
 *
15
 * @package Ichaber\SSSwiftype\Extensions
16
 * @property SiteTree|$this $owner
17
 */
18
class SwiftypeSiteTreeCrawlerExtension extends SiteTreeExtension
19
{
20
    /**
21
     * Urls to crawl
22
     *
23
     * array keyed by getOwnerKey
24
     *
25
     * @var array
26
     */
27
    private $urlsToCrawl = [];
28
29
    /**
30
     * @param array $urls
31
     */
32
    public function setUrlsToCrawl(array $urls) {
33
        $this->urlsToCrawl = $urls;
34
    }
35
36
    /**
37
     * @return array
38
     */
39
    public function getUrlsToCrawl(): array
40
    {
41
        return $this->urlsToCrawl;
42
    }
43
44
    /**
45
     * We need to collate Urls before we write, just in case an author has changed the Page's Url Segment. If they
46
     * have, then we need to request Swiftype to reindex both the old Url (which should then be marked by Swiftype
47
     * as a 404), and the new Url
48
     */
49
    public function onBeforeWrite(): void
50
    {
51
        $this->collateUrls();
52
    }
53
54
    /**
55
     * After a publish has occurred, we can collate and process immediately (no need to split things out like during
56
     * an unpublish)
57
     *
58
     * @param SiteTree|mixed $original
59
     * @return void
60
     */
61
    public function onAfterPublish(&$original): void
62
    {
63
        $this->collateUrls();
64
        $this->processCollatedUrls();
65
66
        // Check to see if the clearing of cache has been disabled (useful for unit testing, or any other reason you
67
        // might have to disable it)
68
        $clearCacheDisabled = Config::inst()->get(static::class, 'clear_cache_disabled');
69
70
        if ($clearCacheDisabled) {
71
            return;
72
        }
73
74
        // It's important that we clear the cache after we have finished requesting reindex from Swiftype
75
        $this->clearCacheSingle();
76
    }
77
78
    /**
79
     * We need to collate the Urls to be purged *before* we complete the unpublish action (otherwise, the LIVE Urls
80
     * will no longer be available, since the page is now unpublished)
81
     */
82
    public function onBeforeUnpublish(): void
83
    {
84
        $this->collateUrls();
85
    }
86
87
    /**
88
     * After the unpublish has completed, we can now request Swiftype to reindex the Urls that we collated
89
     */
90
    public function onAfterUnpublish(): void
91
    {
92
        $this->processCollatedUrls();
93
94
        // Check to see if the clearing of cache has been disabled (useful for unit testing, or any other reason you
95
        // might have to disable it)
96
        $clearCacheDisabled = Config::inst()->get(static::class, 'clear_cache_disabled');
97
98
        if ($clearCacheDisabled) {
99
            return;
100
        }
101
102
        // It's important that we clear the cache after we have finished requesting reindex from Swiftype
103
        $this->clearCacheSingle();
104
    }
105
106
    /**
107
     * You may need to clear the cache at some point during your particular process
108
     *
109
     * Reset all Urls for any/all objects that might be in the cache (keeping in mind that Extensions are singleton,
110
     * so the UrlsToCache could be accessed via singleton and it could contain Urls for many owner objects)
111
     *
112
     * We don't use flushCache (which is called from DataObject) because this is called between write and un/publish,
113
     * and we need our cache to persist through these states
114
     */
115
    public function clearCacheAll(): void
116
    {
117
        $this->setUrlsToCrawl([]);
118
    }
119
120
    /**
121
     * You may need to clear the cache at some point during your particular process
122
     *
123
     * Reset only the Urls related to this particular owner object (keeping in mind that Extensions are singleton,
124
     * so the UrlsToCache could be accessed via singleton and it could contain Urls for many owner objects)
125
     *
126
     * We don't use flushCache (which is called from DataObject) because this is called between write and un/publish,
127
     * and we need our cache to persist through these states
128
     */
129
    public function clearCacheSingle(): void
130
    {
131
        $urls = $this->getUrlsToCrawl();
132
        $key = $this->getOwnerKey();
133
134
        // Nothing for us to do here
135
        if ($key === null) {
136
            return;
137
        }
138
139
        // Nothing for us to do here
140
        if (!array_key_exists($key, $urls)) {
141
            return;
142
        }
143
144
        // Remove this key and it's Urls
145
        unset($urls[$key]);
146
147
        $this->setUrlsToCrawl($urls);
148
    }
149
150
    /**
151
     * Collate Urls to crawl
152
     *
153
     * Extensions are singleton, so we use the owner key to make sure that we're only processing Urls directly related
154
     * to the desired record.
155
     *
156
     * You might need to collate more than one URL per Page (maybe you're using Fluent or another translation module).
157
     * This is the method you will want to override in order to add that additional logic.
158
     */
159
    public function collateUrls(): void
160
    {
161
        // Grab any existing Urls so that we can add to it
162
        $urls = $this->getUrlsToCrawl();
163
164
        // Set us to a LIVE stage/reading_mode
165
        $this->withVersionContext(function() use (&$urls) {
166
            /** @var SiteTree $owner */
167
            $owner = $this->getOwner();
168
            $key = $this->getOwnerKey();
169
170
            // We can't do anything if we don't have a key to use
171
            if ($key === null) {
172
                return;
173
            }
174
175
            // Create a new container for this key
176
            if (!array_key_exists($key, $urls)) {
177
                $urls[$key] = [];
178
            }
179
180
            // Grab the absolute live link without ?stage=Live appended
181
            $link = $owner->getAbsoluteLiveLink(false);
182
183
            // If this record is not published, or we're unable to get a "Live Link" (for whatever reason), then there
184
            // is nothing more we can do here
185
            if (!$link) {
186
                return;
187
            }
188
189
            // Nothing for us to do here, the Link is already being tracked
190
            if (in_array($link, $urls[$key])) {
191
                return;
192
            }
193
194
            // Add our base URL to this key
195
            $urls[$key][] = $link;
196
        });
197
198
        // Update the Urls we have stored for indexing
199
        $this->setUrlsToCrawl($urls);
200
    }
201
202
    /**
203
     * Send requests to Swiftype to reindex each of the Urls that we have previously collated
204
     */
205
    protected function processCollatedUrls(): void
206
    {
207
        // Fetch the Urls that we need to reindex
208
        $key = $this->getOwnerKey();
209
210
        // We can't do anything if we don't have a key to process
211
        if ($key === null) {
212
            return;
213
        }
214
215
        $urls = $this->getUrlsToCrawl();
216
217
        // There is nothing for us to do here if there are no Urls
218
        if (count(array_keys($urls)) === 0) {
219
            return;
220
        }
221
222
        // There are no Urls for this particular key
223
        if (!array_key_exists($key, $urls)) {
224
            return;
225
        }
226
227
        // Force the reindexing of each URL we collated
228
        foreach ($urls[$key] as $url)  {
229
            $this->forceSwiftypeIndex($url);
230
        }
231
    }
232
233
    /**
234
     * @param string $updateUrl
235
     * @return bool
236
     */
237
    protected function forceSwiftypeIndex(string $updateUrl): bool
238
    {
239
        // We don't reindex dev environments
240
        if (Director::isDev()) {
241
            return true;
242
        }
243
244
        $crawler = SwiftypeCrawler::create();
245
246
        return $crawler->send($updateUrl);
247
    }
248
249
    /**
250
     * @return string
251
     */
252
    protected function getOwnerKey(): ?string
253
    {
254
        $owner = $this->owner;
255
256
        // Can't generate a key if the owner has not yet been written to the DB
257
        if (!$owner->isInDB()) {
258
            return null;
259
        }
260
261
        $key = str_replace('\\', '', $owner->ClassName . $owner->ID);
262
263
        return $key;
264
    }
265
266
    /**
267
     * Sets the version context to Live as that's what crawlers will (normally) see
268
     *
269
     * The main function is to suppress the ?stage=Live querystring. LeftAndMain will set the default
270
     * reading mode to 'DRAFT' when initialising so to counter this we need to re-set the default
271
     * reading mode back to LIVE
272
     *
273
     * @param callable $callback
274
     */
275
    private function withVersionContext(callable $callback): void
276
    {
277
        Versioned::withVersionedMode(static function() use ($callback) {
278
            // Grab our current stage and reading mode
279
            $originalDefaultReadingMode = Versioned::get_default_reading_mode();
280
            $originalReadingMode = Versioned::get_reading_mode();
281
            $originalStage = Versioned::get_stage();
282
283
            // Set our stage and reading mode to LIVE
284
            Versioned::set_default_reading_mode('Stage.' . Versioned::LIVE);
285
            Versioned::set_reading_mode('Stage.' . Versioned::LIVE);
286
            Versioned::set_stage(Versioned::LIVE);
287
288
            // Process whatever callback was provided
289
            $callback();
290
291
            // Set us back to the original stage and reading mode
292
            if ($originalReadingMode) {
293
                Versioned::set_default_reading_mode($originalDefaultReadingMode);
294
                Versioned::set_reading_mode($originalReadingMode);
295
            }
296
297
            if ($originalStage) {
298
                Versioned::set_stage($originalStage);
299
            }
300
        });
301
    }
302
}
303