AbstractTransport::isFresh()   B
last analyzed

Complexity

Conditions 6
Paths 5

Size

Total Lines 28
Code Lines 11

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 9
CRAP Score 6.2163

Importance

Changes 0
Metric Value
dl 0
loc 28
ccs 9
cts 11
cp 0.8182
rs 8.439
c 0
b 0
f 0
cc 6
eloc 11
nc 5
nop 2
crap 6.2163
1
<?php
2
3
namespace TreeHouse\Feeder\Transport;
4
5
use Symfony\Component\EventDispatcher\EventDispatcher;
6
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
7
use TreeHouse\Feeder\Event\TransportEvent;
8
use TreeHouse\Feeder\Exception\TransportException;
9
use TreeHouse\Feeder\FeedEvents;
10
11
abstract class AbstractTransport implements TransportInterface
12
{
13
    /**
14
     * File where transport will download to. The file name is generated if this is used.
15
     *
16
     * @var string
17
     *
18
     * @private There's some logic here that requires only this class has access to it.
19
     */
20
    private $destination;
21
22
    /**
23
     * Directory where transport will download to. The file name is generated if this is used.
24
     *
25
     * @var string
26
     */
27
    protected $destinationDir;
28
29
    /**
30
     * The number of seconds that the transport may be cached.
31
     *
32
     * @var int
33
     */
34
    protected $maxAge;
35
36
    /**
37
     * @var Connection
38
     */
39
    protected $connection;
40
41
    /**
42
     * @var EventDispatcherInterface
43
     */
44
    protected $eventDispatcher;
45
46
    /**
47
     * @param Connection               $conn
48
     * @param string|null              $destination
49
     * @param EventDispatcherInterface $dispatcher
50
     */
51 132
    public function __construct(Connection $conn, $destination = null, EventDispatcherInterface $dispatcher = null)
52
    {
53 132
        $this->connection = $conn;
54 132
        $this->destination = $destination;
55 132
        $this->eventDispatcher = $dispatcher ?: new EventDispatcher();
56 132
        $this->maxAge = 86400;
57 132
    }
58
59
    /**
60
     * @inheritdoc
61
     */
62
    public function __clone()
63
    {
64
        $this->destination = null;
65
        $this->connection = clone $this->connection;
66
    }
67
68
    /**
69
     * @inheritdoc
70
     */
71
    public function __toString()
72
    {
73
        return $this->connection->__toString();
74
    }
75
76
    /**
77
     * @inheritdoc
78
     */
79 18
    public function setConnection(Connection $connection)
80
    {
81 18
        $this->connection = $connection;
82 4
    }
83
84
    /**
85
     * @inheritdoc
86
     */
87 8
    public function getConnection()
88
    {
89 4
        return $this->connection;
90 8
    }
91
92
    /**
93
     * @inheritdoc
94
     */
95 6
    public function setEventDispatcher(EventDispatcherInterface $dispatcher)
96
    {
97 6
        $this->eventDispatcher = $dispatcher;
98 6
    }
99
100
    /**
101
     * @inheritdoc
102
     */
103 4
    public function getEventDispatcher()
104
    {
105 4
        return $this->eventDispatcher;
106
    }
107
108
    /**
109
     * @param int $seconds
110
     */
111 4
    public function setMaxAge($seconds)
112
    {
113 4
        $this->maxAge = $seconds;
114 4
    }
115
116
    /**
117
     * @return int
118
     */
119 4
    public function getMaxAge()
120
    {
121 4
        return $this->maxAge;
122
    }
123
124
    /**
125
     * @inheritdoc
126
     */
127 12
    public function setDestination($destination)
128
    {
129 12
        if ($this->destination) {
130 4
            throw new \LogicException(
131
                'Destination is already set and is immutable. If you want to
132
                change the destination, you can clone this transport or create
133
                a new one'
134 4
            );
135
        }
136
137 12
        $this->destination = $destination;
138 12
    }
139
140
    /**
141
     * @inheritdoc
142
     */
143 60
    public function getDestination()
144
    {
145 60
        if (!$this->destination) {
146 56
            $this->destination = $this->getDefaultDestination();
147 56
        }
148
149 60
        return $this->destination;
150
    }
151
152
    /**
153
     * @inheritdoc
154
     */
155 20
    public function setDestinationDir($destinationDir)
156
    {
157 20
        if ($this->destination) {
158 4
            throw new \LogicException(
159
                'Destination is already set and is immutable. If you want to
160
                change the destination directory, you can clone this transport
161
                or create a new one'
162 4
            );
163
        }
164
165 16
        $this->destinationDir = $destinationDir;
166 16
    }
167
168
    /**
169
     * @inheritdoc
170
     */
171 70
    public function getDestinationDir()
172
    {
173 70
        return $this->destinationDir ?: sys_get_temp_dir();
174
    }
175
176
    /**
177
     * @return string
178
     */
179 16
    public function getDefaultDestination()
180
    {
181 16
        return sprintf('%s/%s', rtrim($this->getDestinationDir(), '/'), $this->connection->getHash());
182
    }
183
184
    /**
185
     * @inheritdoc
186
     */
187 56
    public function getFile()
188
    {
189 56
        $maxAge = new \DateTime();
190 56
        $maxAge->sub(new \DateInterval(sprintf('PT%dS', $this->maxAge)));
191
192 56
        return new \SplFileObject($this->fetch($maxAge));
193
    }
194
195
    /**
196
     * @param \DateTime $maxAge
197
     *
198
     * @throws TransportException
199
     *
200
     * @return string
201
     */
202 56
    final public function fetch(\DateTime $maxAge = null)
203
    {
204 56
        $destination = $this->getDestination();
205
206
        // check if we need to download feed
207 56
        $event = new TransportEvent($this);
208 56
        if (!$this->isFresh($destination, $maxAge)) {
209
            // make sure directory exists
210 22
            $dir = dirname($destination);
211 22
            if (!is_dir($dir)) {
212
                if (true !== @mkdir($dir, 0777, true)) {
213
                    throw new TransportException(sprintf('Could not create feed dir "%s"', $dir));
214
                }
215
            }
216
217
            // perform an atomic write operation: first write to a tmp destination, then rename it.
218 22
            $tmpDestination = $this->getTempDestination($destination);
219
220 22
            $this->eventDispatcher->dispatch(FeedEvents::PRE_FETCH, $event);
221
222 22
            $this->doFetch($tmpDestination);
223 16
            if (false === rename($tmpDestination, $destination)) {
224
                unlink($tmpDestination);
225
226
                throw new TransportException(sprintf('Could not rename "%s" to "%s"', $tmpDestination, $destination));
227
            }
228
229 16
            $this->eventDispatcher->dispatch(FeedEvents::POST_FETCH, $event);
230 16
        } else {
231 36
            $this->eventDispatcher->dispatch(FeedEvents::FETCH_CACHED, $event);
232
        }
233
234 50
        return $destination;
235
    }
236
237
    /**
238
     * @return string
239
     */
240 26
    final public static function getDefaultUserAgent()
241
    {
242 26
        return 'Feeder/1.0';
243
    }
244
245
    /**
246
     * Purges a previously transported file, removing the destination and
247
     * whatever cache the transport uses internally.
248
     */
249 4
    public function purge()
250
    {
251 4
        $destination = $this->getDestination();
252
253 4
        if (is_file($destination)) {
254 4
            unlink($destination);
255 4
        }
256 4
    }
257
258
    /**
259
     * @param string $destination
260
     *
261
     * @return string
262
     */
263 22
    protected function getTempDestination($destination)
264
    {
265 22
        return tempnam(dirname($destination), basename($destination));
266
    }
267
268
    /**
269
     * @param string    $destination
270
     * @param \DateTime $maxAge
271
     *
272
     * @return bool
273
     */
274 42
    protected function isFresh($destination, \DateTime $maxAge = null)
275
    {
276
        // fetch if file does not exist
277 42
        if (!file_exists($destination)) {
278 6
            return false;
279
        }
280
281
        // if file exists and no max-age is given, use the cached file
282 38
        if (!$maxAge instanceof \DateTime) {
283
            return true;
284
        }
285
286
        // download if ttl is passed
287 38
        $mtime = new \Datetime('@' . filemtime($destination));
288
289
        // see if cache has expired
290 38
        if ($mtime < $maxAge) {
291
            return false;
292
        }
293
294
        // check with last modified date (if available)
295 38
        if (($lastMod = $this->getLastModifiedDate()) && ($mtime < $lastMod)) {
296 2
            return false;
297
        }
298
299
        // all checks passed, use the cached version
300 36
        return true;
301
    }
302
303
    /**
304
     * Fetches the resource, makes sure a file is present at the given destination.
305
     *
306
     * @param string $destination
307
     */
308
    abstract protected function doFetch($destination);
309
}
310