Passed
Push — master ( ba8c17...e692f4 )
by Nicolaas
02:18
created

SortOutFolders::physicallyMovingImage()   B

Complexity

Conditions 7
Paths 9

Size

Total Lines 19
Code Lines 13

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
eloc 13
c 1
b 0
f 0
dl 0
loc 19
rs 8.8333
cc 7
nc 9
nop 2
1
<?php
2
3
namespace Sunnysideup\PerfectCmsImages\Api;
4
5
use SilverStripe\Control\Director;
6
use SilverStripe\Control\Controller;
7
use SilverStripe\Control\HTTPRequest;
8
use SilverStripe\Dev\BuildTask;
9
use SilverStripe\ORM\DB;
10
11
use SilverStripe\Assets\Image;
12
use SilverStripe\Assets\Folder;
13
14
use Sunnysideup\PerfectCmsImages\Api\PerfectCMSImages;
15
16
use SilverStripe\Core\Config\Config;
17
use SilverStripe\Core\Config\Configurable;
18
19
use SilverStripe\Core\ClassInfo;
20
21
use SilverStripe\Core\Injector\Injector;
22
23
24
/**
25
 * the assumption we make here is that a particular group of images (e.g. Page.Image) live
26
 * live in a particular folder.
27
 */
28
class SortOutFolders
29
{
30
31
    use Configurable;
32
33
    /**
34
     * @var Folder
35
     */
36
    protected $unusedImagesFolder = null;
37
38
    /**
39
     *
40
     * @var bool
41
     */
42
    protected $debug = false;
43
44
    /**
45
     *
46
     * @var bool
47
     */
48
    protected $verbose = true;
49
50
    private static $unused_images_folder_name = 'unusedimages';
51
52
    public function setVerbose(?bool $b = true)
53
    {
54
        $this->verbose = $b;
55
        return $this;
56
    }
57
58
    public function setDebug(?bool $b = true)
59
    {
60
        $this->debug = $b;
61
        return $this;
62
    }
63
64
65
    /**
66
     * @param string $unusedFolderName
67
     * @param array $data
68
     * Create test jobs for the purposes of testing.
69
     * The array must contains arrays with
70
     * - folder
71
     * - used_by
72
     * used_by is an array that has ClassNames and Relations
73
     * (has_one / has_many / many_many relations)
74
     * e.g. Page.Image, MyDataObject.MyImages
75
     *
76
     * @param HTTPRequest $request
77
     */
78
    public function run(string $unusedFolderName, array $data) // phpcs:ignore
79
    {
80
        $this->unusedImagesFolder = Folder::find_or_make($unusedFolderName);
81
82
        $folderArray = $this->getFolderArray($data);
83
        if ($this->verbose) {
84
            DB::alteration_message('==== List of folders ====');
85
            echo '<pre>'.print_r($folderArray, 1).'</pre>';
0 ignored issues
show
Bug introduced by
Are you sure print_r($folderArray, 1) of type string|true can be used in concatenation? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

85
            echo '<pre>'./** @scrutinizer ignore-type */ print_r($folderArray, 1).'</pre>';
Loading history...
86
        }
87
88
        $listOfImageIds = $this->getListOfImages($folderArray);
89
90
        // remove
91
        foreach($listOfImageIds as $folderName => $listOfIds) {
92
            DB::alteration_message('==== DOING '.$folderName.' of Image IDs ===='. count($listOfIds).' images to keep');
93
            $this->removeUnusedFiles($folderName, $listOfIds);
94
        }
95
    }
96
97
98
    public function getFolderArray(array $data) :array
99
    {
100
        // check folders
101
        $folderArray = [];
102
        foreach($data as $dataInner) {
103
            $folder = $dataInner['folder'] ?? '';
104
            if($folder) {
105
                $folderArray[$folder] = [];
106
                $classes = $dataInner['used_by'] ?? [];
107
                if(! empty($classes)) {
108
                    if(is_array($classes)) {
109
                        foreach($classes as $classAndMethodList) {
110
                            $folderArray[$folder][$classAndMethodList] = $classAndMethodList;
111
                        }
112
                    } else {
113
                        user_error('Bad definition for: '.print_r($dataInner, 1));
0 ignored issues
show
Bug introduced by
Are you sure print_r($dataInner, 1) of type string|true can be used in concatenation? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

113
                        user_error('Bad definition for: './** @scrutinizer ignore-type */ print_r($dataInner, 1));
Loading history...
114
                    }
115
                }
116
            }
117
        }
118
        return $folderArray;
119
    }
120
121
    public function getListOfImages(array $folderArray) : array
122
    {
123
        $listOfImageIds = [];
124
        foreach($folderArray as $folderName => $classAndMethodList) {
125
126
            // find all images that should be there...
127
            $listOfIds = [];
128
            foreach($classAndMethodList as $classAndMethod) {
129
                list($className, $method) = explode('.', $classAndMethod);
130
                $fieldDetails = $this->getFieldDetails($className, $method);
131
                if(empty($fieldDetails)) {
132
                    user_error('Could not find relation: '.$className.'.'.$method);
133
                }
134
                if($fieldDetails['dataType'] === 'has_one') {
135
                    $list = $className::get()->columnUnique($method.'ID');
136
                } else {
137
                    $dataClassName = $fieldDetails['dataClassName'];
138
                    $list = $dataClassName::get()->relation($method)->columnUnique('ID');
139
                }
140
                $listOfIds = array_merge(
141
                    $listOfIds,
142
                    $list
143
                );
144
            }
145
            if(count($listOfIds)) {
146
                $listOfImageIds[$folderName] = $listOfIds;
147
            }
148
        }
149
        return $listOfImageIds;
150
    }
151
152
    public function removeUnusedFiles(string $folderName, array $listOfImageIds)
153
    {
154
        $unusedFolderName = $this->unusedImagesFolder->Name;
155
        $folder = Folder::find_or_make($folderName);
156
        $listAsString = implode(',', $listOfImageIds);
157
        $where = ' ParentID = ' . $folder->ID. ' AND File.ID NOT IN('.$listAsString.')';
158
        $unused = Image::get()->where($where);
159
        if ($unused->exists()) {
160
            foreach ($unused as $file) {
161
                $oldName = $file->getFileName();
162
                if($this->verbose) {
163
                    DB::alteration_message('moving '.$file->getFileName().' to '.$unusedFolderName);
164
                }
165
                if($this->debug) {
166
                } else {
167
                    $file->ParentID = $this->unusedImagesFolder->ID;
168
                    $file->write();
169
                    $file->doPublish();
170
                    $newName = str_replace($folder->Name, $unusedFolderName, $oldName);
171
                    if($newName !== $file->getFileName()) {
172
                        DB::alteration_message('ERROR: file names do not match. Compare: '.$newName. ' with ' . $file->getFileName());
173
                    }
174
                    $this->physicallyMovingImage($oldName, $newName);
175
                }
176
            }
177
        }
178
    }
179
180
    public function moveUsedFilesIntoFolder(string $folderName, array $listOfImageIds)
181
    {
182
        $unusedFolderName = $this->unusedImagesFolder->Name;
183
        $folder = Folder::find_or_make($folderName);
184
        $listAsString = implode(',', $listOfImageIds);
185
        $where = ' ParentID <> ' . $folder->ID. ' AND File.ID IN('.$listAsString.')';
186
        $used = Image::get()->where($where);
187
        if ($used->exists()) {
188
            foreach ($used as $file) {
189
                $oldName = $file->getFileName();
190
                if($this->verbose) {
191
                    DB::alteration_message('moving '.$file->getFileName().' to '.$unusedFolderName);
192
                }
193
                if($this->debug) {
194
                } else {
195
                    $file->ParentID = $folder->ID;
196
                    $file->write();
197
                    $file->doPublish();
198
                    $newName = str_replace($unusedFolderName, $folder->Name, $oldName);
199
                    if($this->verbose && $newName !== $file->getFileName()) {
200
                        DB::alteration_message('ERROR: file names do not match. Compare: '.$newName. ' with ' . $file->getFileName(), 'deleted');
201
                    } else {
202
                        $this->physicallyMovingImage($oldName, $newName);
203
                    }
204
                }
205
            }
206
        }
207
    }
208
209
    protected static $my_field_cache = [];
210
211
    protected function getFieldDetails(string $originClassName, string $originMethod) : array
212
    {
213
        $key = $originClassName.'_'.$originMethod;
214
        if(! isset(self::$my_field_cache[$key])) {
215
            $types = ['has_one', 'has_many', 'many_many'];
216
            $classNames = ClassInfo::ancestry($originClassName, true);
217
            foreach ($classNames as $className) {
218
                $obj = Injector::inst()->get($className);
0 ignored issues
show
Unused Code introduced by
The assignment to $obj is dead and can be removed.
Loading history...
219
                foreach ($types as $type) {
220
                    $rels = Config::inst()->get($className, $type, Config::UNINHERITED);
221
                    if (is_array($rels) && ! empty($rels)) {
222
                        foreach ($rels as $relName => $relType) {
223
                            if (Image::class === $relType && $relName === $originMethod) {
224
                                self::$my_field_cache[$key] = [
225
                                    'dataClassName' => $className,
226
                                    'dataType' => $type,
227
                                ];
228
                            }
229
                        }
230
                    }
231
                }
232
            }
233
        }
234
        return self::$my_field_cache[$key];
235
    }
236
237
    protected function physicallyMovingImage(string $oldName, string $newName)
238
    {
239
        if ($oldName !== $newName) {
240
            $oldNameFull = Controller::join_links(ASSETS_PATH, $oldName);
241
            $newNameFull = Controller::join_links(ASSETS_PATH, $newName);
242
            if (file_exists($oldNameFull)) {
243
                if(file_exists($newNameFull)) {
244
                    if ($this->verbose) {
245
                        DB::alteration_message('Deleting '.$newName.' to make place for a new file.', 'deleted');
246
                    }
247
                    unlink($newNameFull);
248
                }
249
                if ($this->verbose) {
250
                    DB::alteration_message('Moving '.$oldNameFull.' to '.$newNameFull, 'created');
251
                }
252
                rename($oldNameFull, $newNameFull);
253
            }
254
        } elseif($this->verbose) {
255
            DB::alteration_message('ERROR: old and new file names are the same '.$oldName, 'deleted');
256
        }
257
    }
258
259
}
260