Completed
Pull Request — master (#47)
by Helpful
02:11
created

StaticPagesQueue::get_next_url()   B

Complexity

Conditions 4
Paths 4

Size

Total Lines 23
Code Lines 12

Duplication

Lines 0
Ratio 0 %
Metric Value
dl 0
loc 23
rs 8.7972
cc 4
eloc 12
nc 4
nop 0
1
<?php
2
/**
3
 * This class responsibility is twofold:
4
 * 1) Holding the data for a prioritized queue of URLs that needs to be static cached
5
 * 2) Interaction with that queue
6
 *
7
 * @TODO: would be good to refactor this queue to hold not only URLSegment, but also ClassName and ID of the
8
 * associated object (or any other metadata). This would allow FilesystemPublisher::publishPages and others
9
 * to stop having to smuggle the metadata within the URL (see URLArrayData::get_object).
10
 *
11
 */
12
class StaticPagesQueue extends DataObject
13
{
14
15
    /**
16
     *
17
     * @var array
18
     */
19
    public static $create_table_options = array(
20
        'MySQLDatabase' => 'ENGINE=InnoDB'
21
    );
22
23
    /**
24
     *
25
     * @var array
26
     */
27
    public static $db = array(
28
        'Priority' => 'Int',
29
        'URLSegment' => 'Varchar(255)',
30
        'Freshness' => "Enum('stale, regenerating, error', 'stale')"
31
    );
32
33
    /**
34
     *
35
     * @var array
36
     */
37
    public static $defaults = array(
38
        "Priority" => 3
39
    );
40
41
    /**
42
     *
43
     * @var array
44
     */
45
    public static $default_sort = "\"Priority\"";
46
47
    /**
48
     * Sets database indexes
49
     *
50
     * @var array
51
     */
52
    public static $indexes = array(
53
        'freshness_priority_created' => '(Freshness, Priority, Created)',
54
    );
55
56
    /**
57
     *
58
     * @var boolean
59
     */
60
    private static $realtime = false;
61
62
    /**
63
     *
64
     * @var int
65
     */
66
    protected static $minutes_until_force_regeneration = 1;
67
68
    /**
69
     *
70
     * @var array
71
     */
72
    protected static $insert_statements = array();
73
74
    /**
75
     *
76
     * @var array
77
     */
78
    protected static $urls = array();
79
    
80
    /**
81
     *
82
     * @return bool
83
     */
84
    public static function is_realtime()
85
    {
86
        return Config::inst()->get('StaticPagesQueue', 'realtime');
87
    }
88
89
    /**
90
     *
91
     * @param type $priority
92
     * @param type $URLSegment
93
     * @return type
94
     */
95
    public static function add_to_queue($priority, $URLSegment)
96
    {
97
        $now = date("Y-m-d H:i:s");
98
        self::$insert_statements[$URLSegment] = '(\''.$now.'\',\''.$now.'\', \''.Convert::raw2sql($priority).'\',\''.Convert::raw2sql($URLSegment).'\')';
99
        self::$urls[md5($URLSegment)] = $URLSegment;
100
    }
101
102
        /**
103
     * This will push all the currently cached insert statements to be pushed 
104
     * into the database
105
     *
106
     * @return void
107
     */
108
    public static function push_urls_to_db()
109
    {
110
        foreach (self::$insert_statements as $stmt) {
111
            $insertSQL = 'INSERT INTO "StaticPagesQueue" ("Created", "LastEdited", "Priority", "URLSegment") VALUES ' . $stmt;
112
            DB::query($insertSQL);
113
        }
114
        self::remove_old_cache(self::$urls);
115
        // Flush the cache so DataObject::get works correctly
116
        if (!empty(self::$insert_statements) && DB::affectedRows()) {
0 ignored issues
show
Deprecated Code introduced by
The method DB::affectedRows() has been deprecated with message: since version 4.0 Use DB::affected_rows instead

This method has been deprecated. The supplier of the class has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the method will be removed from the class and what other method or class to use instead.

Loading history...
117
            singleton(__CLASS__)->flushCache();
118
        }
119
        self::$insert_statements = array();
120
    }
121
    
122
    /**
123
     * Remove an object by the url
124
     *
125
     * @param string $URLSegment
126
     * @return bool - if there was an queue item removed
127
     *
128
     */
129
    public static function delete_by_link($URLSegment)
130
    {
131
        $object = self::get_by_link($URLSegment);
132
        if (!$object) {
133
            return false;
134
        }
135
136
        $object->delete();
137
        unset($object);
138
        return true;
139
    }
140
    
141
    /**
142
     * Update the queue with the information that this url renders an error somehow
143
     *
144
     * @param string $url
145
     */
146
    public static function has_error($url)
147
    {
148
        if (!$url) {
149
            return;
150
        }
151
        
152
        $existingObject = self::get_by_link($url);
153
        $existingObject->Freshness = 'error';
154
        $existingObject->write();
155
    }
156
157
    /**
158
     * Returns a single queue object according to a particular priority and freshness measure.
159
     * This method removes any duplicates and makes the object as "regenerating", so other calls to this method
160
     * don't grab the same object.
161
     * If we are using MySQLDatabase with InnoDB, we do row-level locking when updating the dataobject to allow for
162
     * distributed cache rebuilds
163
     * @static
164
     * @param $freshness
165
     * @param $sortOrder
166
     */
167
    protected static function get_queue_object($freshness, $interval = null, $sortOrder = array('Priority'=>'DESC', 'ID'=>'ASC'))
168
    {
169
        $className = __CLASS__;
170
        $queueObject = null;
171
        $filterQuery = array("Freshness" => $freshness);
172
        if ($interval) {
173
            $filterQuery["LastEdited:LessThan"] = $interval;
174
        }
175
176
        $query = self::get();
177
        if ($query->Count() > 0) {
178
            $offset = 0;
179
            $filteredQuery = $query->filter($filterQuery)->sort($sortOrder);
180
181
            if ($filteredQuery->Count() > 0) {
182
                if (DB::getConn() instanceof MySQLDatabase) {   //locking currently only works on MySQL
0 ignored issues
show
Deprecated Code introduced by
The method DB::getConn() has been deprecated with message: since version 4.0 Use DB::get_conn instead

This method has been deprecated. The supplier of the class has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the method will be removed from the class and what other method or class to use instead.

Loading history...
183
184
                    do {
185
                        $queueObject = $filteredQuery->limit(1, $offset)->first();   //get first item
186
187
                        if ($queueObject) {
188
                            $lockName = md5($queueObject->URLSegment . $className);
189
                        }
190
                        //try to locking the item's URL, keep trying new URLs until we find one that is free to lock
191
                        $offset++;
192
                    } while ($queueObject && !LockMySQL::isFreeToLock($lockName));
0 ignored issues
show
Bug introduced by
The variable $lockName does not seem to be defined for all execution paths leading up to this point.

If you define a variable conditionally, it can happen that it is not defined for all execution paths.

Let’s take a look at an example:

function myFunction($a) {
    switch ($a) {
        case 'foo':
            $x = 1;
            break;

        case 'bar':
            $x = 2;
            break;
    }

    // $x is potentially undefined here.
    echo $x;
}

In the above example, the variable $x is defined if you pass “foo” or “bar” as argument for $a. However, since the switch statement has no default case statement, if you pass any other value, the variable $x would be undefined.

Available Fixes

  1. Check for existence of the variable explicitly:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        if (isset($x)) { // Make sure it's always set.
            echo $x;
        }
    }
    
  2. Define a default value for the variable:

    function myFunction($a) {
        $x = ''; // Set a default which gets overridden for certain paths.
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        echo $x;
    }
    
  3. Add a value for the missing path:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
    
            // We add support for the missing case.
            default:
                $x = '';
                break;
        }
    
        echo $x;
    }
    
Loading history...
193
194
                    if ($queueObject) {
195
                        $lockSuccess = LockMySQL::getLock($lockName);  //acquire a lock with the URL of the queue item we have just fetched
196
                        if ($lockSuccess) {
197
                            self::remove_duplicates($queueObject->ID);  //remove any duplicates
198
                            self::mark_as_regenerating($queueObject);   //mark as regenerating so nothing else grabs it
199
                            LockMySQL::releaseLock($lockName);            //return the object and release the lock
200
                        }
201
                    }
202
                } else {
203
                    $queueObject = $filteredQuery->first();
204
                    self::remove_duplicates($queueObject->ID);
205
                    self::mark_as_regenerating($queueObject);
206
                }
207
            }
208
        }
209
210
        return $queueObject;    //return the object or null
211
    }
212
213
    /**
214
     * Finds the next most prioritized url that needs recaching
215
     *
216
     * @return string
217
     */
218
    public static function get_next_url()
219
    {
220
        $object = self::get_queue_object('stale');
221
        if ($object) {
222
            return $object->URLSegment;
223
        }
224
225
        $interval = date('Y-m-d H:i:s', strtotime('-'.self::$minutes_until_force_regeneration.' minutes'));
226
227
        // Find URLs that has been stuck in regeneration
228
        $object = self::get_queue_object('regenerating', $interval);
229
        if ($object) {
230
            return $object->URLSegment;
231
        }
232
233
        // Find URLs that is erronous and might work now (flush issues etc)
234
        $object = self::get_queue_object('error', $interval);
235
        if ($object) {
236
            return $object->URLSegment;
237
        }
238
239
        return '';
240
    }
241
242
    /**
243
     * Removes the .html fresh copy of the cache.
244
     * Keeps the *.stale.html copy in place,
245
     * in order to notify the user of the stale content.
246
     *
247
     * @param array $URLSegments
248
     */
249
    protected static function remove_old_cache(array $URLSegments)
250
    {
251
        $publisher = singleton('SiteTree')->getExtensionInstance('FilesystemPublisher');
252
        if ($publisher) {
253
            $paths = $publisher->urlsToPaths($URLSegments);
254
            foreach ($paths as $absolutePath) {
255
                if (!file_exists($publisher->getDestDir().'/'.$absolutePath)) {
256
                    continue;
257
                }
258
259
                unlink($publisher->getDestDir().'/'.$absolutePath);
260
            }
261
        }
262
    }
263
264
    /**
265
     * Mark this current StaticPagesQueue as a work in progress
266
     *
267
     * @param StaticPagesQueue $object 
268
     */
269
    protected static function mark_as_regenerating(StaticPagesQueue $object)
270
    {
271
        $now = date('Y-m-d H:i:s');
272
        DB::query('UPDATE "StaticPagesQueue" SET "LastEdited" = \''.$now.'\', "Freshness"=\'regenerating\' WHERE "ID" = '.$object->ID);
273
        singleton(__CLASS__)->flushCache();
274
    }
275
276
    /**
277
     * Removes all duplicates that has the same URLSegment as $ID
278
     *
279
     * @param int $ID - ID of the object whose duplicates we want to remove
280
     * @return void
281
     */
282
    public static function remove_duplicates($ID)
283
    {
284
        $obj = DataObject::get_by_id('StaticPagesQueue', $ID);
285
        if (!$obj) {
286
            return 0;
287
        }
288
        DB::query(
289
            sprintf('DELETE FROM "StaticPagesQueue" WHERE "URLSegment" = \'%s\' AND "ID" != %d', $obj->URLSegment, (int)$ID)
290
        );
291
    }
292
293
    /**
294
     *
295
     * @param string $url
296
     * @param bool $onlyStale - Get only stale entries
0 ignored issues
show
Bug introduced by
There is no parameter named $onlyStale. Was it maybe removed?

This check looks for PHPDoc comments describing methods or function parameters that do not exist on the corresponding method or function.

Consider the following example. The parameter $italy is not defined by the method finale(...).

/**
 * @param array $germany
 * @param array $island
 * @param array $italy
 */
function finale($germany, $island) {
    return "2:1";
}

The most likely cause is that the parameter was removed, but the annotation was not.

Loading history...
297
     * @return DataObject || false - The first item matching the query
298
     */
299
    protected static function get_by_link($url)
300
    {
301
        $filter = '"URLSegment" = \''.Convert::raw2sql($url).'\'';
302
        $res = DB::query('SELECT * FROM "StaticPagesQueue" WHERE '.$filter.' LIMIT 1;');
303
        if (!$res->numRecords()) {
304
            return false;
305
        }
306
        return new StaticPagesQueue($res->first());
307
    }
308
}
309