Article::setupRels()   A
last analyzed

Complexity

Conditions 1
Paths 1

Size

Total Lines 21

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
c 0
b 0
f 0
dl 0
loc 21
rs 9.584
cc 1
nc 1
nop 0
1
<?php
2
3
namespace Alpha\Model;
4
5
use Alpha\Model\Type\SmallText;
6
use Alpha\Model\Type\DEnum;
7
use Alpha\Model\Type\Text;
8
use Alpha\Model\Type\LargeText;
9
use Alpha\Model\Type\Boolean;
10
use Alpha\Model\Type\Relation;
11
use Alpha\Util\Config\Configprovider;
12
use Alpha\Util\Logging\Logger;
13
use Alpha\Util\Service\ServiceFactory;
14
use Alpha\Exception\ValidationException;
15
use Alpha\Exception\FileNotFoundException;
16
use Alpha\Exception\AlphaException;
17
use Alpha\Controller\Front\FrontController;
18
19
/**
20
 * An article class for the CMS.
21
 *
22
 * @since 1.0
23
 *
24
 * @author John Collins <[email protected]>
25
 * @license http://www.opensource.org/licenses/bsd-license.php The BSD License
26
 * @copyright Copyright (c) 2018, John Collins (founder of Alpha Framework).
27
 * All rights reserved.
28
 *
29
 * <pre>
30
 * Redistribution and use in source and binary forms, with or
31
 * without modification, are permitted provided that the
32
 * following conditions are met:
33
 *
34
 * * Redistributions of source code must retain the above
35
 *   copyright notice, this list of conditions and the
36
 *   following disclaimer.
37
 * * Redistributions in binary form must reproduce the above
38
 *   copyright notice, this list of conditions and the
39
 *   following disclaimer in the documentation and/or other
40
 *   materials provided with the distribution.
41
 * * Neither the name of the Alpha Framework nor the names
42
 *   of its contributors may be used to endorse or promote
43
 *   products derived from this software without specific
44
 *   prior written permission.
45
 *
46
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND
47
 * CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES,
48
 * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
49
 * MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
50
 * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR
51
 * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
52
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
53
 * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
54
 * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
55
 * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
56
 * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE
57
 * OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
58
 * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
59
 * </pre>
60
 */
61
class Article extends ActiveRecord
62
{
63
    /**
64
     * The article title.
65
     *
66
     * @var \Alpha\Model\Type\SmallText
67
     *
68
     * @since 1.0
69
     */
70
    protected $title;
71
72
    /**
73
     * The description of the article.
74
     *
75
     * @var \Alpha\Model\Type\SmallText
76
     *
77
     * @since 1.0
78
     */
79
    protected $description;
80
81
    /**
82
     * Optional custom body onload Javascript.
83
     *
84
     * @var \Alpha\Model\Type\SmallText
85
     *
86
     * @since 1.0
87
     */
88
    protected $bodyOnload;
89
90
    /**
91
     * Any custom HTML header content (e.g. Javascript) for the article.
92
     *
93
     * @var \Alpha\Model\Type\Text
94
     *
95
     * @since 1.0
96
     */
97
    protected $headerContent;
98
99
    /**
100
     * The article content.
101
     *
102
     * @var \Alpha\Model\Type\LargeText
103
     *
104
     * @since 1.0
105
     */
106
    protected $content;
107
108
    /**
109
     * The author of the article.
110
     *
111
     * @var \Alpha\Model\Type\SmallText
112
     *
113
     * @since 1.0
114
     */
115
    protected $author;
116
117
    /**
118
     * A boolean to control whether the artcile is publically accessible or not.
119
     *
120
     * @var \Alpha\Model\Type\Boolean
121
     *
122
     * @since 1.0
123
     */
124
    protected $published;
125
126
    /**
127
     * A Relation containing all of the comments on this article.
128
     *
129
     * @var \Alpha\Model\Type\Relation
130
     *
131
     * @since 1.0
132
     */
133
    protected $comments;
134
135
    /**
136
     * A Relation containing all of the votes on this article.
137
     *
138
     * @var \Alpha\Model\Type\Relation
139
     *
140
     * @since 1.0
141
     */
142
    protected $votes;
143
144
    /**
145
     * A Relation containing all of the tags on this article.
146
     *
147
     * @var \Alpha\Model\Type\Relation
148
     *
149
     * @since 1.0
150
     */
151
    protected $tags;
152
153
    /**
154
     * An array of all of the attributes on this Record which are tagged.
155
     *
156
     * @var array
157
     *
158
     * @since 1.0
159
     */
160
    protected $taggedAttributes = array('title', 'description', 'content');
161
162
    /**
163
     * Path to a .text file where the content of this article is stored (optional).
164
     *
165
     * @var string
166
     *
167
     * @since 1.0
168
     */
169
    private $filePath;
170
171
    /**
172
     * An array of data display labels for the class properties.
173
     *
174
     * @var array
175
     *
176
     * @since 1.0
177
     */
178
    protected $dataLabels = array('ID' => 'Article ID#', 'title' => 'Title', 'description' => 'Description', 'bodyOnload' => 'Body onload Javascript', 'content' => 'Content', 'headerContent' => 'HTML Header Content', 'author' => 'Author', 'created_ts' => 'Date Added', 'updated_ts' => 'Date of last Update', 'published' => 'Published', 'URL' => 'URL', 'printURL' => 'Printer version URL', 'comments' => 'Comments', 'votes' => 'Votes', 'tags' => 'Tags');
179
180
    /**
181
     * The name of the database table for the class.
182
     *
183
     * @var string
184
     *
185
     * @since 1.0
186
     */
187
    const TABLE_NAME = 'Article';
188
189
    /**
190
     * The URL for this article (transient).
191
     *
192
     * @var string
193
     *
194
     * @since 1.0
195
     */
196
    protected $URL;
197
198
    /**
199
     * The print URL for this article (transient).
200
     *
201
     * @var string
202
     *
203
     * @since 1.0
204
     */
205
    protected $printURL;
206
207
    /**
208
     * Trace logger.
209
     *
210
     * @var \Alpha\Util\Logging\Logger
211
     *
212
     * @since 1.0
213
     */
214
    private static $logger = null;
0 ignored issues
show
Comprehensibility introduced by
Consider using a different property name as you override a private property of the parent class.
Loading history...
215
216
    /**
217
     * The constructor which sets up some housekeeping attributes.
218
     *
219
     * @since 1.0
220
     */
221
    public function __construct()
222
    {
223
        self::$logger = new Logger('Article');
224
225
        $config = ConfigProvider::getInstance();
226
        $separator = $config->get('cms.url.title.separator');
227
228
        // ensure to call the parent constructor
229
        parent::__construct();
230
231
        $this->title = new SmallText();
232
        $this->title->setHelper('Please provide a title for the article. Note that the '.$separator.' character is not allowed!');
233
        $this->title->setSize(100);
234
        $this->title->setRule('/^[^'.$separator.']*$/');
235
236
        $this->description = new SmallText();
237
        $this->description->setHelper('Please provide a brief description of the article.');
238
        $this->description->setSize(200);
239
        $this->description->setRule("/\w+/");
240
        $this->bodyOnload = new SmallText();
241
        $this->content = new LargeText();
242
        $this->headerContent = new Text();
243
        $this->author = new SmallText();
244
        $this->author->setHelper('Please state the name of the author of this article');
245
        $this->author->setSize(70);
246
        $this->author->setRule("/\w+/");
247
        $this->published = new Boolean(0);
0 ignored issues
show
Documentation introduced by
0 is of type integer, but the function expects a boolean.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
248
249
        $this->comments = new Relation();
250
        $this->markTransient('comments');
251
252
        $this->votes = new Relation();
253
        $this->markTransient('votes');
254
255
        $this->tags = new Relation();
256
        $this->markTransient('tags');
257
258
        $this->URL = '';
259
        $this->printURL = '';
260
        // mark the URL attributes as transient
261
        $this->markTransient('URL');
262
        $this->markTransient('printURL');
263
264
        // mark title as unique
265
        $this->markUnique('title');
266
267
        $this->markTransient('filePath');
268
        $this->markTransient('taggedAttributes');
269
270
        $this->setupRels();
271
    }
272
273
    /**
274
     * After creating a new Article, tokenize the description field to form a set
275
     * of automated tags and save them.
276
     *
277
     * @since 1.0
278
     */
279
    protected function after_save_callback()
280
    {
281
        if ($this->getVersion() == 1 && $this->tags instanceof \Alpha\Model\Type\Relation) {
282
            // update the empty tags values to reference this ID
283
            $this->tags->setValue($this->ID);
284
285
            foreach ($this->taggedAttributes as $tagged) {
286
                $tags = Tag::tokenize($this->get($tagged), 'Alpha\Model\Article', $this->getID());
287
                foreach ($tags as $tag) {
288
                    try {
289
                        $tag->save();
290
                    } catch (ValidationException $e) {
0 ignored issues
show
Coding Style Comprehensibility introduced by
Consider adding a comment why this CATCH block is empty.
Loading history...
291
                        /*
292
                         * The unique key has most-likely been violated because this Record is already tagged with this
293
                         * value, so we can ignore in this case.
294
                         */
295
                    }
296
                }
297
            }
298
        }
299
300
        $this->setupRels();
301
    }
302
303
    /**
304
     * Set up the transient URL attributes for the artcile after it has loaded.
305
     *
306
     * @since 1.0
307
     */
308
    protected function after_loadByAttribute_callback()
309
    {
310
        $this->{'after_load_callback'}();
311
    }
312
313
    /**
314
     * Set up the transient URL attributes for the article after it has loaded.
315
     *
316
     * @since 1.0
317
     */
318
    protected function after_load_callback()
319
    {
320
        $config = ConfigProvider::getInstance();
321
322
        $this->URL = $config->get('app.url').'/a/'.str_replace(' ', $config->get('cms.url.title.separator'), $this->title->getValue());
323
324
        $this->printURL = $config->get('app.url').'/a/'.str_replace(' ', $config->get('cms.url.title.separator'), $this->title->getValue()).'/print';
325
326
        $this->setupRels();
327
    }
328
329
    /**
330
     * Gets an array of the IDs of the most recent articles added to the system (by date), from the newest
331
     * article to the amount specified by the $limit.
332
     *
333
     * @param int    $limit
334
     *
335
     * @return array
336
     *
337
     * @since 1.0
338
     *
339
     * @throws \Alpha\Exception\AlphaException
340
     */
341
    public function loadRecentWithLimit($limit)
342
    {
343
        $sqlQuery = 'SELECT ID FROM '.$this->getTableName()." WHERE published='1' ORDER BY created_ts DESC LIMIT 0, $limit;";
344
345
        $result = $this->query($sqlQuery);
346
347
        $IDs = array();
348
349
        foreach ($result as $row) {
350
            array_push($IDs, $row['ID']);
351
        }
352
353
        return $IDs;
354
    }
355
356
    /**
357
     * Generates the location of the attachments folder for this article.
358
     *
359
     * @return string
360
     *
361
     * @since 1.0
362
     */
363
    public function getAttachmentsLocation()
364
    {
365
        $config = ConfigProvider::getInstance();
366
367
        return $config->get('app.file.store.dir').'attachments/article_'.$this->getID();
368
    }
369
370
    /**
371
     * Generates the URL of the attachments folder for this article.
372
     *
373
     * @return string
374
     *
375
     * @since 1.0
376
     */
377
    public function getAttachmentsURL()
378
    {
379
        $config = ConfigProvider::getInstance();
380
381
        return $config->get('app.url').'/attachments/article_'.$this->getID();
382
    }
383
384
    /**
385
     * Generates a secure URL for downloading an attachment file via the ViewAttachment controller.
386
     *
387
     * @param string $filename
388
     *
389
     * @since 1.0
390
     */
391
    public function getAttachmentSecureURL($filename)
392
    {
393
        return FrontController::generateSecureURL('act=Alpha\\Controller\\AttachmentController&articleID='.$this->getID().'&filename='.$filename);
394
    }
395
396
    /**
397
     * Creates the attachment folder for the article on the server.
398
     *
399
     * @since 1.0
400
     *
401
     * @throws \Alpha\Exception\AlphaException
402
     */
403
    public function createAttachmentsFolder()
404
    {
405
        // create the attachment directory for the article
406
        try {
407
            mkdir($this->getAttachmentsLocation());
408
        } catch (\Exception $e) {
409
            throw new AlphaException('Unable to create the folder ['.$this->getAttachmentsLocation().'] for the article.');
410
        }
411
412
        // ...and set write permissions on the folder
413
        try {
414
            chmod($this->getAttachmentsLocation(), 0777);
415
        } catch (\Exception $e) {
416
            throw new AlphaException('Unable to set write permissions on the folder ['.$this->getAttachmentsLocation().'].');
417
        }
418
    }
419
420
    /**
421
     * Method for returning the calculated score for this article.
422
     *
423
     * @return string
424
     *
425
     * @since 1.0
426
     */
427
    public function getArticleScore()
428
    {
429
        $votes = $this->getArticleVotes();
430
431
        $score = 0;
432
        $total_score = 0;
433
        $vote_count = count($votes);
434
435
        for ($i = 0; $i < $vote_count; ++$i) {
436
            $total_score += $votes[$i]->get('score');
437
        }
438
439
        if ($vote_count > 0) {
440
            $score = $total_score/$vote_count;
441
        }
442
443
        return sprintf('%01.2f', $score);
444
    }
445
446
    /**
447
     * Method for fetching all of the votes for this article.
448
     *
449
     * @return array An array of ArticleVote objects
450
     *
451
     * @since 1.0
452
     */
453
    public function getArticleVotes()
454
    {
455
        $votes = $this->votes->getRelated();
456
457
        return $votes;
458
    }
459
460
    /**
461
     * Method to determine if the logged-in user has already voted for this article.
462
     *
463
     * @return bool True if they have voted already, false otherwise
464
     *
465
     * @since 1.0
466
     *
467
     * @throws \Alpha\Exception\AlphaException
468
     */
469
    public function checkUserVoted()
470
    {
471
        $config = ConfigProvider::getInstance();
472
        $sessionProvider = $config->get('session.provider.name');
473
        $session = ServiceFactory::getInstance($sessionProvider, 'Alpha\Util\Http\Session\SessionProviderInterface');
474
        // just going to return true if nobody is logged in
475
        if ($session->get('currentUser') == null) {
476
            return true;
477
        }
478
479
        $userID = $session->get('currentUser')->getID();
480
481
        $vote = new ArticleVote();
482
483
        $sqlQuery = 'SELECT COUNT(*) AS usersVote FROM '.$vote->getTableName()." WHERE articleID='".$this->ID."' AND personID='".$userID."';";
484
485
        $result = $this->query($sqlQuery);
486
487
        if (!isset($result[0])) {
488
            throw new AlphaException('Failed to check if the current user voted for the article ['.$this->ID.'], query ['.$sqlQuery.']');
489
        }
490
491
        $row = $result[0];
492
493
        if ($row['usersVote'] == '0') {
494
            return false;
495
        } else {
496
            return true;
497
        }
498
    }
499
500
    /**
501
     * Method for fetching all of the comments for this article.
502
     *
503
     * @return array An array of ArticleComment objects
504
     *
505
     * @since 1.0
506
     */
507
    public function getArticleComments()
508
    {
509
        $comments = $this->comments->getRelated();
510
511
        return $comments;
512
    }
513
514
    /**
515
     * Loads the content of the ArticleObject from the specified file path.
516
     *
517
     * @param string $filePath
518
     *
519
     * @since 1.0
520
     *
521
     * @throws \Alpha\Exception\FileNotFoundException
522
     */
523
    public function loadContentFromFile($filePath)
524
    {
525
        try {
526
            $this->content->setValue(file_get_contents($filePath));
527
            $this->filePath = $filePath;
528
        } catch (\Exception $e) {
529
            throw new FileNotFoundException($e->getMessage());
530
        }
531
    }
532
533
    /**
534
     * Returns true if the article content was loaded from a .text file, false otherwise.
535
     *
536
     * @return bool
537
     *
538
     * @since 1.0
539
     */
540
    public function isLoadedFromFile()
541
    {
542
        return $this->filePath == '' ? false : true;
543
    }
544
545
    /**
546
     * Returns the timestamp of when the content .text file for this article was last
547
     * modified.
548
     *
549
     * @return string
550
     *
551
     * @since 1.0
552
     *
553
     * @throws \Alpha\Exception\FileNotFoundException
554
     */
555
    public function getContentFileDate()
556
    {
557
        if ($this->filePath != '') {
558
            try {
559
                return date('Y-m-d H:i:s', filemtime($this->filePath));
560
            } catch (\Exception $e) {
561
                throw new FileNotFoundException($e->getMessage());
562
            }
563
        } else {
564
            throw new FileNotFoundException('Error trying to access an article content file when none is set!');
565
        }
566
    }
567
568
    /**
569
     * Sets up the Relation definitions on this record object.
570
     *
571
     * @since 2.0
572
     */
573
    protected function setupRels()
574
    {
575
        $this->comments->setValue($this->ID);
576
        $this->comments->setRelatedClass('Alpha\Model\ArticleComment');
577
        $this->comments->setRelatedClassField('articleID');
578
        $this->comments->setRelatedClassDisplayField('content');
579
        $this->comments->setRelationType('ONE-TO-MANY');
580
581
        $this->votes->setValue($this->ID);
582
        $this->votes->setRelatedClass('Alpha\Model\ArticleVote');
583
        $this->votes->setRelatedClassField('articleID');
584
        $this->votes->setRelatedClassDisplayField('score');
585
        $this->votes->setRelationType('ONE-TO-MANY');
586
587
        $this->tags->setRelatedClass('Alpha\Model\Tag');
588
        $this->tags->setRelatedClassField('taggedID');
589
        $this->tags->setRelatedClassDisplayField('content');
590
        $this->tags->setRelationType('ONE-TO-MANY');
591
        $this->tags->setTaggedClass(get_class($this));
592
        $this->tags->setValue($this->ID);
593
    }
594
}
595