Passed
Push — master ( 62280a...198bab )
by Yannick
08:17
created

ImportAccessUrlCommand   A

Complexity

Total Complexity 18

Size/Duplication

Total Lines 147
Duplicated Lines 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
eloc 88
c 1
b 0
f 0
dl 0
loc 147
rs 10
wmc 18

3 Methods

Rating   Name   Duplication   Size   Complexity  
A configure() 0 7 1
A __construct() 0 4 1
D execute() 0 126 16
1
<?php
2
// src/CoreBundle/Command/ImportAccessUrlCommand.php
3
4
namespace Chamilo\CoreBundle\Command;
5
6
use Chamilo\CoreBundle\Entity\AccessUrl;
7
use Chamilo\CoreBundle\Entity\User;
8
use Chamilo\CoreBundle\Entity\ResourceType;
9
use Doctrine\ORM\EntityManagerInterface;
10
use PhpOffice\PhpSpreadsheet\IOFactory;
11
use Symfony\Component\Console\Command\Command;
12
use Symfony\Component\Console\Input\InputArgument;
13
use Symfony\Component\Console\Input\InputInterface;
14
use Symfony\Component\Console\Output\OutputInterface;
15
use Symfony\Component\Console\Style\SymfonyStyle;
16
17
class ImportAccessUrlCommand extends Command
18
{
19
    protected static $defaultName = 'chamilo:import-access-url';
20
21
    private EntityManagerInterface $entityManager;
22
23
    public function __construct(EntityManagerInterface $entityManager)
24
    {
25
        parent::__construct();
26
        $this->entityManager = $entityManager;
27
    }
28
29
    protected function configure(): void
30
    {
31
        $this
32
            ->setDescription('Import AccessUrl entities from an XLSX file')
33
            ->addArgument('xlsx-file', InputArgument::REQUIRED, 'Path to the XLSX file')
34
            ->addArgument('base-url', InputArgument::REQUIRED, 'Base URL for subdomains (e.g., https://somedomain.com/)')
35
            ->setHelp('This command imports AccessUrl entities from an XLSX file. The file must have a title row with "subdomain" and "description" columns. Subdomains are lowercased. The ResourceNode parent is set to AccessUrl ID = 1.');
36
    }
37
38
    protected function execute(InputInterface $input, OutputInterface $output): int
39
    {
40
        $io = new SymfonyStyle($input, $output);
41
        $io->title('Importing AccessUrl Entities');
42
43
        $xlsxFile = $input->getArgument('xlsx-file');
44
        $baseUrl = $input->getArgument('base-url');
45
46
        // Validate base URL
47
        $parsedUrl = parse_url($baseUrl);
48
        if (!isset($parsedUrl['host']) || !isset($parsedUrl['scheme'])) {
49
            $io->error("Invalid base URL: {$baseUrl}. Must include scheme and host (e.g., https://somedomain.com/).");
50
            return Command::FAILURE;
51
        }
52
        $mainDomain = $parsedUrl['host']; // e.g., 'somedomain.com'
53
        $scheme = $parsedUrl['scheme']; // e.g., 'https'
54
55
        // Validate XLSX file
56
        if (!file_exists($xlsxFile) || !is_readable($xlsxFile)) {
57
            $io->error("XLSX file not found or not readable: {$xlsxFile}");
58
            return Command::FAILURE;
59
        }
60
61
        // Get default user (ID 1)
62
        $defaultUserId = 1;
63
        $user = $this->entityManager->getRepository(User::class)->find($defaultUserId);
64
        if (!$user) {
65
            $io->error("User with ID {$defaultUserId} not found. Cannot assign to URLs.");
66
            return Command::FAILURE;
67
        }
68
69
        // Get main AccessUrl (ID = 1) and its ResourceNode
70
        $mainAccessUrl = $this->entityManager->getRepository(AccessUrl::class)->find(1);
71
        if (!$mainAccessUrl) {
72
            $io->error("Main AccessUrl with ID = 1 not found.");
73
            return Command::FAILURE;
74
        }
75
        $parentResourceNode = $mainAccessUrl->getResourceNode();
76
        if (!$parentResourceNode) {
77
            $io->error("Main AccessUrl (ID = 1) has no associated ResourceNode.");
78
            return Command::FAILURE;
79
        }
80
81
        // Get ResourceType for AccessUrl
82
        $resourceTypeRepo = $this->entityManager->getRepository(ResourceType::class);
83
        $resourceType = $resourceTypeRepo->findOneBy(['title' => 'urls']);
84
        if (!$resourceType) {
85
            $io->error("ResourceType 'urls' not found.");
86
            return Command::FAILURE;
87
        }
88
89
        try {
90
            // Load the Excel file
91
            $spreadsheet = IOFactory::load($xlsxFile);
92
            $worksheet = $spreadsheet->getActiveSheet();
93
            $rows = $worksheet->toArray();
94
95
            // Find column indices for 'subdomain' and 'description'
96
            $headerRow = array_shift($rows); // Assume first row is header
97
            $subdomainIndex = array_search('subdomain', $headerRow);
98
            $descriptionIndex = array_search('description', $headerRow);
99
100
            if ($subdomainIndex === false || $descriptionIndex === false) {
101
                $io->error("Columns 'subdomain' or 'description' not found in the Excel file.");
102
                return Command::FAILURE;
103
            }
104
105
            $createdCount = 0;
106
            $skippedCount = 0;
107
108
            foreach ($rows as $rowNumber => $row) {
109
                $subdomain = trim($row[$subdomainIndex] ?? ''); // 'subdomain' column
110
                $description = trim($row[$descriptionIndex] ?? ''); // 'description' column
111
112
                // Skip empty subdomain
113
                if (empty($subdomain)) {
114
                    $io->warning("Row " . ($rowNumber + 2) . ": Skipping due to empty subdomain.");
115
                    $skippedCount++;
116
                    continue;
117
                }
118
119
                // Lowercase and validate subdomain
120
                $subdomain = strtolower($subdomain);
121
                if (!preg_match('/^[a-z0-9][a-z0-9\-]*[a-z0-9]$/', $subdomain)) {
122
                    $io->warning("Row " . ($rowNumber + 2) . ": Invalid subdomain '$subdomain'. Skipping.");
123
                    $skippedCount++;
124
                    continue;
125
                }
126
127
                // Construct the subdomain URL
128
                $subdomainUrl = "{$scheme}://{$subdomain}.{$mainDomain}/";
129
130
                // Check if the URL already exists
131
                $existingUrl = $this->entityManager->getRepository(AccessUrl::class)->findOneBy(['url' => $subdomainUrl]);
132
                if ($existingUrl) {
133
                    $io->warning("Row " . ($rowNumber + 2) . ": URL {$subdomainUrl} already exists. Skipping.");
134
                    $skippedCount++;
135
                    continue;
136
                }
137
138
                // Create new AccessUrl entity
139
                $accessUrl = new AccessUrl();
140
                $accessUrl->setUrl($subdomainUrl);
141
                $accessUrl->setDescription($description);
142
                $accessUrl->setActive(1); // Set as active
143
                $accessUrl->setCreatedBy($defaultUserId); // Set created_by
144
                $accessUrl->setTms(new \DateTime()); // Set timestamp
145
                //$accessUrl->setResourceNode($resourceNode); // Assign ResourceNode
146
                $accessUrl->addUser($user); // Associate user
147
                $accessUrl->setCreator($user); // Temporary hack as AccessUrl should be able to set this automatically
148
149
                // Persist entity
150
                $this->entityManager->persist($accessUrl);
151
152
                $io->success("Row " . ($rowNumber + 2) . ": Created URL: {$subdomainUrl} with description: {$description}, assigned to user ID: {$defaultUserId}, parent AccessUrl ID: 1");
153
                $createdCount++;
154
            }
155
156
            // Save changes
157
            $this->entityManager->flush();
158
            $io->success("Import completed: {$createdCount} URLs created, {$skippedCount} rows skipped.");
159
160
            return Command::SUCCESS;
161
        } catch (\Exception $e) {
162
            $io->error("Error: " . $e->getMessage());
163
            return Command::FAILURE;
164
        }
165
    }
166
}
167