Passed
Pull Request — master (#6395)
by
unknown
09:17
created

CreateCoursesFromStructuredFileCommand::execute()   C

Complexity

Conditions 14
Paths 30

Size

Total Lines 155
Code Lines 104

Duplication

Lines 0
Ratio 0 %

Importance

Changes 3
Bugs 0 Features 0
Metric Value
cc 14
eloc 104
c 3
b 0
f 0
nc 30
nop 2
dl 0
loc 155
rs 5.0133

How to fix   Long Method    Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

1
<?php
2
3
/* For licensing terms, see /license.txt */
4
5
declare(strict_types=1);
6
7
namespace Chamilo\CoreBundle\Command;
8
9
use Chamilo\CoreBundle\Entity\Course;
10
use Chamilo\CoreBundle\Entity\ResourceNode;
11
use Chamilo\CoreBundle\Entity\User;
12
use Chamilo\CoreBundle\Service\CourseService;
13
use Chamilo\CoreBundle\Settings\SettingsManager;
14
use Chamilo\CourseBundle\Entity\CDocument;
15
use Chamilo\CourseBundle\Entity\CLp;
16
use Chamilo\CourseBundle\Entity\CLpItem;
17
use Doctrine\ORM\EntityManagerInterface;
18
use Symfony\Component\Console\Attribute\AsCommand;
19
use Symfony\Component\Console\Command\Command;
20
use Symfony\Component\Console\Input\InputArgument;
21
use Symfony\Component\Console\Input\InputInterface;
22
use Symfony\Component\Console\Input\InputOption;
23
use Symfony\Component\Console\Output\OutputInterface;
24
use Symfony\Component\Console\Style\SymfonyStyle;
25
use Symfony\Component\DependencyInjection\ParameterBag\ParameterBagInterface;
26
use Symfony\Component\Finder\Finder;
27
28
#[AsCommand(
29
    name: 'app:create-courses-from-structured-file',
30
    description: 'Create courses and learning paths from a folder containing files.
31
If permissions like 0660/0770 are used, it is recommended to run this command as www-data:
32
  sudo -u www-data php bin/console app:create-courses-from-structured-file /path/to/folder',
33
)]
34
class CreateCoursesFromStructuredFileCommand extends Command
35
{
36
    private const MAX_COURSE_LENGTH_CODE = 40;
37
38
    public function __construct(
39
        private readonly EntityManagerInterface $em,
40
        private readonly CourseService $courseService,
41
        private readonly SettingsManager $settingsManager,
42
        private readonly ParameterBagInterface $parameterBag,
43
    ) {
44
        parent::__construct();
45
    }
46
47
    protected function configure(): void
48
    {
49
        $this
50
            ->addArgument(
51
                'folder',
52
                InputArgument::REQUIRED,
53
                'Absolute path to the folder that contains course files'
54
            )
55
            ->addOption(
56
                'user',
57
                null,
58
                InputOption::VALUE_OPTIONAL,
59
                'Expected user owner of created files (e.g. www-data)',
60
                'www-data'
61
            );
62
    }
63
64
    protected function execute(InputInterface $input, OutputInterface $output): int
65
    {
66
        $io = new SymfonyStyle($input, $output);
67
68
        $expectedUser = $input->getOption('user');
69
        $realUser = get_current_user();
70
71
        if ($realUser !== $expectedUser) {
72
            $io->warning("You are running this command as '$realUser', but expected user is '$expectedUser'.If file permissions are too restrictive (e.g. 0660), the web server may not be able to access the files.
73
            To avoid this issue, consider running the command like this: sudo -u {$expectedUser} php bin/console app:create-courses-from-structured-file /path/to/folder");
74
        }
75
76
        $adminUser = $this->getFirstAdmin();
77
        if (!$adminUser) {
78
            $io->error('No admin user found in the system.');
79
            return Command::FAILURE;
80
        }
81
82
        $folder = $input->getArgument('folder');
83
        if (!is_dir($folder)) {
84
            $io->error("Invalid folder: $folder");
85
            return Command::FAILURE;
86
        }
87
88
        // Retrieve Unix permissions from platform settings
89
        $dirPermOct = octdec($this->settingsManager->getSetting('document.permissions_for_new_directories') ?? '0777');
90
        $filePermOct = octdec($this->settingsManager->getSetting('document.permissions_for_new_files') ?? '0666');
91
92
        // Absolute base to /var/upload/resource
93
        $uploadBase = $this->parameterBag->get('kernel.project_dir') . '/var/upload/resource';
94
95
        $finder = new Finder();
96
        $finder->files()->in($folder);
97
98
        foreach ($finder as $file) {
99
            $basename = $file->getBasename();
100
            $filename = pathinfo($basename, PATHINFO_FILENAME);
101
            $filePath = $file->getRealPath();
102
103
            // Parse filename: expected format "1234=Name-of-course"
104
            $parts = explode('=', $filename, 2);
105
            if (count($parts) !== 2) {
106
                $io->warning("Invalid filename format (expected 'code=Name'): $basename");
107
                continue;
108
            }
109
            $codePart = $parts[0];
110
            $namePart = $parts[1];
111
112
            // Code: remove dashes/spaces, uppercase everything
113
            $rawCode = $codePart . strtoupper(str_replace(['-', ' '], '', $namePart));
114
            $courseCode = $this->generateUniqueCourseCode($rawCode);
115
116
            // Title: replace dashes with spaces
117
            $courseTitle = str_replace('-', ' ', $namePart);
118
119
            $io->section("Creating course: $courseCode");
120
121
            // 2. Create course
122
            $course = $this->courseService->createCourse([
123
                'title' => $courseTitle,
124
                'wanted_code' => $courseCode,
125
                'add_user_as_teacher' => true,
126
                'course_language' => $this->settingsManager->getSetting('language.platform_language'),
127
                'visibility' => Course::OPEN_PLATFORM,
128
                'subscribe' => true,
129
                'unsubscribe' => true,
130
                'disk_quota' => $this->settingsManager->getSetting('document.default_document_quotum'),
131
                'expiration_date' => (new \DateTime('+1 year'))->format('Y-m-d H:i:s'),
132
            ]);
133
134
            if (!$course) {
135
                throw new \RuntimeException("Course '$courseCode' could not be created.");
136
            }
137
138
            // 3. Create learning path
139
            $lp = (new CLp())
140
                ->setLpType(1)
141
                ->setTitle($courseTitle)
142
                ->setDescription('')
143
                ->setPublishedOn(null)
144
                ->setExpiredOn(null)
145
                ->setCategory(null)
146
                ->setParent($course)
147
                ->addCourseLink($course)
148
                ->setCreator($adminUser);
149
150
            $this->em->getRepository(CLp::class)->createLp($lp);
151
152
            // 4. Create document
153
            $document = (new CDocument())
154
                ->setFiletype('file')
155
                ->setTitle($basename)
156
                ->setComment(null)
157
                ->setReadonly(false)
158
                ->setCreator($adminUser)
159
                ->setParent($course)
160
                ->addCourseLink($course);
161
162
            $this->em->persist($document);
163
            $this->em->flush();
164
165
            $documentRepo = $this->em->getRepository(CDocument::class);
166
            $resourceFile = $documentRepo->addFileFromPath($document, $basename, $filePath);
167
168
            // 4.1  Apply permissions to the real file & its directory
169
            if ($resourceFile) {
170
                $resourceNodeRepo = $this->em->getRepository(ResourceNode::class);
171
                $relativePath = $resourceNodeRepo->getFilename($resourceFile);
172
                $fullPath = realpath($uploadBase . $relativePath);
173
174
                if ($fullPath && is_file($fullPath)) {
175
                    @chmod($fullPath, $filePermOct);
176
                }
177
                $fullDir = dirname($fullPath ?: '');
178
                if ($fullDir && is_dir($fullDir)) {
179
                    @chmod($fullDir, $dirPermOct);
180
                }
181
            }
182
183
            // 5. Ensure learning path root item exists
184
            $lpItemRepo = $this->em->getRepository(CLpItem::class);
185
            $rootItem = $lpItemRepo->getRootItem((int) $lp->getIid());
186
187
            if (!$rootItem) {
188
                $rootItem = (new CLpItem())
189
                    ->setTitle('root')
190
                    ->setPath('root')
191
                    ->setLp($lp)
192
                    ->setItemType('root');
193
                $this->em->persist($rootItem);
194
                $this->em->flush();
195
            }
196
197
            // 6. Create LP item linked to the document
198
            $lpItem = (new CLpItem())
199
                ->setLp($lp)
200
                ->setTitle($basename)
201
                ->setItemType('document')
202
                ->setRef((string) $document->getIid())
203
                ->setPath((string) $document->getIid())
204
                ->setDisplayOrder(1)
205
                ->setLaunchData('')
206
                ->setMinScore(0)
207
                ->setMaxScore(100)
208
                ->setParent($rootItem)
209
                ->setLvl(1)
210
                ->setRoot($rootItem);
211
212
            $this->em->persist($lpItem);
213
            $this->em->flush();
214
215
            $io->success("Course '$courseCode' created with LP and document '$basename'");
216
        }
217
218
        return Command::SUCCESS;
219
    }
220
221
    /**
222
     * Return the first user that has ROLE_ADMIN.
223
     */
224
    private function getFirstAdmin(): ?User
225
    {
226
        return $this->em->getRepository(User::class)
227
            ->createQueryBuilder('u')
228
            ->where('u.roles LIKE :role')
229
            ->setParameter('role', '%ROLE_ADMIN%')
230
            ->setMaxResults(1)
231
            ->getQuery()
232
            ->getOneOrNullResult();
233
    }
234
235
    /**
236
     * Generates a unique course code based on a base string, ensuring DB uniqueness.
237
     */
238
    private function generateUniqueCourseCode(string $baseCode): string
239
    {
240
        $baseCode = substr($baseCode, 0, self::MAX_COURSE_LENGTH_CODE);
241
        $repository = $this->em->getRepository(Course::class);
242
243
        $original = $baseCode;
244
        $suffix = 0;
245
        $tryLimit = 100;
246
247
        do {
248
            $codeToTry = $suffix > 0
249
                ? substr($original, 0, self::MAX_COURSE_LENGTH_CODE - strlen((string) $suffix)) . $suffix
250
                : $original;
251
252
            $exists = $repository->createQueryBuilder('c')
253
                ->select('1')
254
                ->where('c.code = :code')
255
                ->setParameter('code', $codeToTry)
256
                ->setMaxResults(1)
257
                ->getQuery()
258
                ->getOneOrNullResult();
259
260
            if (!$exists) {
261
                return $codeToTry;
262
            }
263
264
            $suffix++;
265
        } while ($suffix < $tryLimit);
266
267
        throw new \RuntimeException("Unable to generate unique course code for base: $baseCode");
268
    }
269
}
270