Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
0.00% covered (danger)
0.00%
0 / 24
0.00% covered (danger)
0.00%
0 / 3
CRAP
0.00% covered (danger)
0.00%
0 / 1
MediaMapper
0.00% covered (danger)
0.00%
0 / 24
0.00% covered (danger)
0.00%
0 / 3
12
0.00% covered (danger)
0.00%
0 / 1
 getByVirtualPath
0.00% covered (danger)
0.00%
0 / 7
0.00% covered (danger)
0.00%
0 / 1
2
 getParentCollection
0.00% covered (danger)
0.00%
0 / 8
0.00% covered (danger)
0.00%
0 / 1
2
 countInternalReferences
0.00% covered (danger)
0.00%
0 / 9
0.00% covered (danger)
0.00%
0 / 1
2
1<?php
2/**
3 * Jingga
4 *
5 * PHP Version 8.1
6 *
7 * @package   Modules\Media\Models
8 * @copyright Dennis Eichhorn
9 * @license   OMS License 2.0
10 * @version   1.0.0
11 * @link      https://jingga.app
12 */
13declare(strict_types=1);
14
15namespace Modules\Media\Models;
16
17use Modules\Admin\Models\AccountMapper;
18use Modules\Tag\Models\TagMapper;
19use phpOMS\DataStorage\Database\Mapper\DataMapperFactory;
20use phpOMS\DataStorage\Database\Mapper\ReadMapper;
21use phpOMS\DataStorage\Database\Query\Builder;
22
23/**
24 * Media mapper class.
25 *
26 * @package Modules\Media\Models
27 * @license OMS License 2.0
28 * @link    https://jingga.app
29 * @since   1.0.0
30 *
31 * @template T of Media
32 * @extends DataMapperFactory<T>
33 */
34class MediaMapper extends DataMapperFactory
35{
36    /**
37     * Columns.
38     *
39     * @var array<string, array{name:string, type:string, internal:string, autocomplete?:bool, readonly?:bool, writeonly?:bool, annotations?:array}>
40     * @since 1.0.0
41     */
42    public const COLUMNS = [
43        'media_id'                  => ['name' => 'media_id',              'type' => 'int',               'internal' => 'id'],
44        'media_name'                => ['name' => 'media_name',            'type' => 'string',            'internal' => 'name',        'autocomplete' => true],
45        'media_description'         => ['name' => 'media_description',     'type' => 'string',            'internal' => 'description', 'autocomplete' => true],
46        'media_description_raw'     => ['name' => 'media_description_raw', 'type' => 'string',            'internal' => 'descriptionRaw'],
47        'media_content'             => ['name' => 'media_content', 'type' => 'int',            'internal' => 'content'],
48        'media_versioned'           => ['name' => 'media_versioned',       'type' => 'bool',              'internal' => 'isVersioned'],
49        'media_status'              => ['name' => 'media_status',          'type' => 'int',              'internal' => 'status'],
50        'media_file'                => ['name' => 'media_file',            'type' => 'string',            'internal' => 'path',        'autocomplete' => true],
51        'media_virtual'             => ['name' => 'media_virtual',         'type' => 'string',            'internal' => 'virtualPath', 'autocomplete' => true],
52        'media_absolute'            => ['name' => 'media_absolute',        'type' => 'bool',              'internal' => 'isAbsolute'],
53        'media_encrypted'           => ['name' => 'media_encrypted',           'type' => 'bool',            'internal' => 'isEncrypted'],
54        'media_password'            => ['name' => 'media_password',        'type' => 'string',            'internal' => 'password'],
55        'media_extension'           => ['name' => 'media_extension',       'type' => 'string',            'internal' => 'extension'],
56        'media_size'                => ['name' => 'media_size',            'type' => 'int',               'internal' => 'size'],
57        'media_source'              => ['name' => 'media_source',      'type' => 'int',              'internal' => 'source'],
58        'media_class'               => ['name' => 'media_class',      'type' => 'int',              'internal' => 'class'],
59        'media_language'            => ['name' => 'media_language',       'type' => 'string',            'internal' => 'language'],
60        'media_country'             => ['name' => 'media_country',       'type' => 'string',            'internal' => 'country'],
61        'media_unit'                => ['name' => 'media_unit',      'type' => 'int',               'internal' => 'unit',   'readonly' => true],
62        'media_created_by'          => ['name' => 'media_created_by',      'type' => 'int',               'internal' => 'createdBy',   'readonly' => true],
63        'media_created_at'          => ['name' => 'media_created_at',      'type' => 'DateTimeImmutable', 'internal' => 'createdAt',   'readonly' => true],
64    ];
65
66    /**
67     * Belongs to.
68     *
69     * @var array<string, array{mapper:class-string, external:string, column?:string, by?:string}>
70     * @since 1.0.0
71     */
72    public const BELONGS_TO = [
73        'createdBy' => [
74            'mapper'   => AccountMapper::class,
75            'external' => 'media_created_by',
76        ],
77    ];
78
79    /**
80     * Belongs to.
81     *
82     * @var array<string, array{mapper:class-string, external:string, by?:string, column?:string, conditional?:bool}>
83     * @since 1.0.0
84     */
85    public const OWNS_ONE = [
86        'source' => [
87            'mapper'   => self::class,
88            'external' => 'media_source',
89        ],
90        'content' => [
91            'mapper'   => MediaContentMapper::class,
92            'external' => 'media_content',
93        ],
94    ];
95
96    /**
97     * Has many relation.
98     *
99     * @var array<string, array{mapper:class-string, table:string, self?:?string, external?:?string, column?:string}>
100     * @since 1.0.0
101     */
102    public const HAS_MANY = [
103        'tags'         => [
104            'mapper'   => TagMapper::class,
105            'table'    => 'media_tag',
106            'external' => 'media_tag_dst',
107            'self'     => 'media_tag_src',
108        ],
109        'types'         => [
110            'mapper'   => MediaTypeMapper::class,
111            'table'    => 'media_type_rel',
112            'external' => 'media_type_rel_dst',
113            'self'     => 'media_type_rel_src',
114        ],
115    ];
116
117    /**
118     * Model to use by the mapper.
119     *
120     * @var class-string<T>
121     * @since 1.0.0
122     */
123    public const MODEL = Media::class;
124
125    /**
126     * Primary table.
127     *
128     * @var string
129     * @since 1.0.0
130     */
131    public const TABLE = 'media';
132
133    /**
134     * Created at.
135     *
136     * @var string
137     * @since 1.0.0
138     */
139    public const CREATED_AT = 'media_created_at';
140
141    /**
142     * Primary field name.
143     *
144     * @var string
145     * @since 1.0.0
146     */
147    public const PRIMARYFIELD = 'media_id';
148
149    /**
150     * Get media based on virtual path.
151     *
152     * The virtual path is equivalent to the directory path on a file system.
153     *
154     * A media model also has a file path, this however doesn't have to be the same as the virtual path
155     * and in fact most of the time it is different. This is because the location on a hard drive or web
156     * drive should not have any impact on the media file/media structure in the application.
157     *
158     * As a result media files are structured by virutal path in the app, by file path on the file system
159     * and by Collections which can have sub-collections as well. Collections allow to reference files
160     * in a different virtual path and are therfore similar to "symlinks", except that they don't reference
161     * a file but create a new virtual media model which groups other media models together in a new virtual
162     * path if so desired without deleting or moving the orginal media files.
163     *
164     * @param string $virtualPath Virtual path
165     * @param int    $status      Media status
166     *
167     * @return ReadMapper
168     *
169     * @since 1.0.0
170     */
171    public static function getByVirtualPath(string $virtualPath = '/', int $status = MediaStatus::NORMAL) : ReadMapper
172    {
173        return self::getAll()
174            ->with('createdBy')
175            ->with('source')
176            ->with('tags')
177            ->with('tags/title')
178            ->where('virtualPath', $virtualPath)
179            ->where('status', $status);
180    }
181
182    /**
183     * Get parent collection
184     *
185     * @param string $path Virtual path
186     *
187     * @return ReadMapper
188     *
189     * @since 1.0.0
190     */
191    public static function getParentCollection(string $path = '/') : ReadMapper
192    {
193        $virtualPath = '/' . \trim(\substr($path, 0, \strripos($path, '/') + 1), '/');
194        $name        = \substr($path, \strripos($path, '/') + 1);
195
196        return CollectionMapper::get()
197            ->with('sources')
198            ->with('source')
199            ->where('virtualPath', $virtualPath)
200            ->where('class', MediaClass::COLLECTION)
201            ->where('name', $name);
202    }
203
204    /**
205     * Check how many references exist to a certain media id.
206     *
207     * This can be helpful to check if a media element can be deleted without damaging other references.
208     *
209     * @param int $id Media id to check references to
210     *
211     * @return int
212     *
213     * @since 1.0.0
214     */
215    public static function countInternalReferences(int $id) : int
216    {
217        $references = self::count()
218            ->where('source', $id)
219            ->execute();
220
221        $query = new Builder(self::$db);
222
223        /** @var array $result */
224        $result = $query->count(self::TABLE)
225            ->where('media_relation_src', '=', $id)
226            ->execute()
227            ?->fetch() ?? [];
228
229        return $references + ((int) ($result[0] ?? 0));
230    }
231}