diff --git a/app/Console/Commands/RegisterFungiOccurences.php b/app/Console/Commands/RegisterFungiOccurences.php index e80cabc..64e82b5 100644 --- a/app/Console/Commands/RegisterFungiOccurences.php +++ b/app/Console/Commands/RegisterFungiOccurences.php @@ -109,7 +109,7 @@ private function fetchIUCNData(string $genus, string $species): int return RedListClassification::NA->value; } } - + private function buildFungiData(array $row): array { return [ diff --git a/app/Console/Kernel.php b/app/Console/Kernel.php index e6b9960..b22352e 100644 --- a/app/Console/Kernel.php +++ b/app/Console/Kernel.php @@ -2,6 +2,7 @@ namespace App\Console; +use App\Jobs\UpdateTaxonomyFromMycoBank; use Illuminate\Console\Scheduling\Schedule; use Illuminate\Foundation\Console\Kernel as ConsoleKernel; @@ -12,7 +13,7 @@ class Kernel extends ConsoleKernel */ protected function schedule(Schedule $schedule): void { - // $schedule->command('inspire')->hourly(); + $schedule->job(UpdateTaxonomyFromMycoBank::class)->monthlyOn(1); } /** diff --git a/app/Jobs/UpdateTaxonomyFromMycoBank.php b/app/Jobs/UpdateTaxonomyFromMycoBank.php new file mode 100644 index 0000000..325961f --- /dev/null +++ b/app/Jobs/UpdateTaxonomyFromMycoBank.php @@ -0,0 +1,92 @@ +downloadFungiList(); + + $reader = ReaderEntityFactory::createXLSXReader(); + + try { + $reader->open($filepath); + $localTaxa = $this->getLocalTaxonomies(); + + foreach ($reader->getSheetIterator() as $sheet) { + $header = null; + + foreach ($sheet->getRowIterator() as $rowIndex => $row) { + $cells = $row->toArray(); + + // Map header columns on first row + if ($rowIndex === 1) { + $header = array_flip($cells); + continue; + } + + if (!$header || !isset($header['Taxon name'])) { + Log::warning('Invalid sheet format: missing "Taxon name" column.'); + break; + } + + $taxonName = trim($cells[$header['Taxon name']] ?? ''); + if ($taxonName === '') { + continue; + } + + $fungi = $localTaxa[$taxonName] ?? null; + if (!$fungi) { + continue; + } + + $this->updateFungiRecord($fungi, $cells, $header); + } + } + } catch (\Throwable $e) { + Log::error('MycoBank import failed: ' . $e->getMessage(), ['trace' => $e->getTraceAsString()]); + } finally { + $reader->close(); + } + } + private function getLocalTaxonomies(): array + { + return Taxonomy::all() + ->mapWithKeys(fn($t) => [$t->genus . ' ' . $t->specie => $t]) + ->toArray(); + } + + private function updateFungiRecord(array $fungiData, array $cells, array $header): void + { + $fungi = Taxonomy::find($fungiData['id']); + $mycoBankId = $cells[$header['MycoBank #']] ?? null; + + $updates = []; + + if (!$fungi) { + Log::warning("Taxonomy not found for ID: {$fungiData['id']}"); + return; + } + + if ($mycoBankId && $mycoBankId !== $fungi->external_id) { + $updates['external_id'] = $mycoBankId; + } + + if (!empty($updates)) { + $fungi->fill($updates)->save(); + } + } +} diff --git a/app/Services/Platforms/MycoBank/MycoBank.php b/app/Services/Platforms/MycoBank/MycoBank.php new file mode 100644 index 0000000..1942066 --- /dev/null +++ b/app/Services/Platforms/MycoBank/MycoBank.php @@ -0,0 +1,78 @@ +get('https://www.mycobank.org/Images/MBList.zip', [ + 'sink' => $name + ]); + + return $this->extractXlsxFromZip($name); + } + + public function extractXlsxFromZip($zipPath) + { + $destinationPath = str_replace('.zip', '', $zipPath); + if (!file_exists($destinationPath)) { + mkdir($destinationPath, 0777, true); + } + + $zip = new \ZipArchive; + if ($zip->open($zipPath) === true) { + $tempPath = storage_path('app/temp_unzip_' . uniqid()); + mkdir($tempPath, 7777, true); + + $zip->extractTo($tempPath); + $zip->close(); + + $xlsxFile = collect(scandir($tempPath)) + ->first(fn($file) => str_ends_with($file, '.xlsx')); + + if ($xlsxFile) { + $xlsxSource = $tempPath . '/' . $xlsxFile; + $xlsxTarget = $destinationPath . '/' . $xlsxFile; + + rename($xlsxSource, $xlsxTarget); + $this->deleteDirectory($tempPath); + + return $xlsxTarget; + } else { + $this->deleteDirectory($tempPath); + throw new \Exception("Nenhum arquivo XLSX encontrado no zip."); + } + } else { + throw new \Exception("Não foi possível abrir o arquivo ZIP."); + } + } + + private function deleteDirectory($dir) + { + if (!file_exists($dir)) return true; + if (!is_dir($dir)) return unlink($dir); + + foreach (scandir($dir) as $item) { + if ($item == '.' || $item == '..') continue; + $this->deleteDirectory($dir . DIRECTORY_SEPARATOR . $item); + } + return rmdir($dir); + } +} diff --git a/composer.json b/composer.json index b6916fc..d171d9b 100644 --- a/composer.json +++ b/composer.json @@ -2,15 +2,20 @@ "name": "laravel/laravel", "type": "project", "description": "The skeleton application for the Laravel framework.", - "keywords": ["laravel", "framework"], + "keywords": [ + "laravel", + "framework" + ], "license": "MIT", "require": { "php": "^8.1", + "box/spout": "^3.3", "guzzlehttp/guzzle": "^7.2", "laravel/framework": "^10.10", "laravel/sanctum": "^3.3", "laravel/tinker": "^2.8", - "phpoffice/phpspreadsheet": "^2.0", + "phpoffice/phpspreadsheet": "^5.1", + "shuchkin/simplexlsx": "^1.1", "tymon/jwt-auth": "^2.1" }, "require-dev": { diff --git a/composer.lock b/composer.lock index a1668f7..c06f0c9 100644 --- a/composer.lock +++ b/composer.lock @@ -4,8 +4,83 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "6e283700ac0665520cc8af4f4af95e03", + "content-hash": "abdc99ed6da995662c8956e5ff05da10", "packages": [ + { + "name": "box/spout", + "version": "v3.3.0", + "source": { + "type": "git", + "url": "https://github.com/box/spout.git", + "reference": "9bdb027d312b732515b884a341c0ad70372c6295" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/box/spout/zipball/9bdb027d312b732515b884a341c0ad70372c6295", + "reference": "9bdb027d312b732515b884a341c0ad70372c6295", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "ext-xmlreader": "*", + "ext-zip": "*", + "php": ">=7.2.0" + }, + "require-dev": { + "friendsofphp/php-cs-fixer": "^2", + "phpunit/phpunit": "^8" + }, + "suggest": { + "ext-iconv": "To handle non UTF-8 CSV files (if \"php-intl\" is not already installed or is too limited)", + "ext-intl": "To handle non UTF-8 CSV files (if \"iconv\" is not already installed)" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.1.x-dev" + } + }, + "autoload": { + "psr-4": { + "Box\\Spout\\": "src/Spout" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "Apache-2.0" + ], + "authors": [ + { + "name": "Adrien Loison", + "email": "adrien@box.com" + } + ], + "description": "PHP Library to read and write spreadsheet files (CSV, XLSX and ODS), in a fast and scalable way", + "homepage": "https://www.github.com/box/spout", + "keywords": [ + "OOXML", + "csv", + "excel", + "memory", + "odf", + "ods", + "office", + "open", + "php", + "read", + "scale", + "spreadsheet", + "stream", + "write", + "xlsx" + ], + "support": { + "issues": "https://github.com/box/spout/issues", + "source": "https://github.com/box/spout/tree/v3.3.0" + }, + "abandoned": true, + "time": "2021-05-14T21:18:09+00:00" + }, { "name": "brick/math", "version": "0.12.1", @@ -135,6 +210,85 @@ ], "time": "2023-12-11T17:09:12+00:00" }, + { + "name": "composer/pcre", + "version": "3.3.2", + "source": { + "type": "git", + "url": "https://github.com/composer/pcre.git", + "reference": "b2bed4734f0cc156ee1fe9c0da2550420d99a21e" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/composer/pcre/zipball/b2bed4734f0cc156ee1fe9c0da2550420d99a21e", + "reference": "b2bed4734f0cc156ee1fe9c0da2550420d99a21e", + "shasum": "" + }, + "require": { + "php": "^7.4 || ^8.0" + }, + "conflict": { + "phpstan/phpstan": "<1.11.10" + }, + "require-dev": { + "phpstan/phpstan": "^1.12 || ^2", + "phpstan/phpstan-strict-rules": "^1 || ^2", + "phpunit/phpunit": "^8 || ^9" + }, + "type": "library", + "extra": { + "phpstan": { + "includes": [ + "extension.neon" + ] + }, + "branch-alias": { + "dev-main": "3.x-dev" + } + }, + "autoload": { + "psr-4": { + "Composer\\Pcre\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Jordi Boggiano", + "email": "j.boggiano@seld.be", + "homepage": "http://seld.be" + } + ], + "description": "PCRE wrapping library that offers type-safe preg_* replacements.", + "keywords": [ + "PCRE", + "preg", + "regex", + "regular expression" + ], + "support": { + "issues": "https://github.com/composer/pcre/issues", + "source": "https://github.com/composer/pcre/tree/3.3.2" + }, + "funding": [ + { + "url": "https://packagist.com", + "type": "custom" + }, + { + "url": "https://github.com/composer", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/composer/composer", + "type": "tidelift" + } + ], + "time": "2024-11-12T16:29:46+00:00" + }, { "name": "dflydev/dot-access-data", "version": "v3.0.2", @@ -2728,19 +2882,20 @@ }, { "name": "phpoffice/phpspreadsheet", - "version": "2.1.0", + "version": "5.1.0", "source": { "type": "git", "url": "https://github.com/PHPOffice/PhpSpreadsheet.git", - "reference": "dbed77bd3a0f68f96c0dd68ad4499d5674fecc3e" + "reference": "fd26e45a814e94ae2aad0df757d9d1739c4bf2e0" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/PHPOffice/PhpSpreadsheet/zipball/dbed77bd3a0f68f96c0dd68ad4499d5674fecc3e", - "reference": "dbed77bd3a0f68f96c0dd68ad4499d5674fecc3e", + "url": "https://api.github.com/repos/PHPOffice/PhpSpreadsheet/zipball/fd26e45a814e94ae2aad0df757d9d1739c4bf2e0", + "reference": "fd26e45a814e94ae2aad0df757d9d1739c4bf2e0", "shasum": "" }, "require": { + "composer/pcre": "^1||^2||^3", "ext-ctype": "*", "ext-dom": "*", "ext-fileinfo": "*", @@ -2757,21 +2912,22 @@ "maennchen/zipstream-php": "^2.1 || ^3.0", "markbaker/complex": "^3.0", "markbaker/matrix": "^3.0", - "php": "^8.0", + "php": "^8.1", "psr/http-client": "^1.0", "psr/http-factory": "^1.0", "psr/simple-cache": "^1.0 || ^2.0 || ^3.0" }, "require-dev": { "dealerdirect/phpcodesniffer-composer-installer": "dev-main", - "dompdf/dompdf": "^2.0", + "dompdf/dompdf": "^2.0 || ^3.0", "friendsofphp/php-cs-fixer": "^3.2", "mitoteam/jpgraph": "^10.3", "mpdf/mpdf": "^8.1.1", "phpcompatibility/php-compatibility": "^9.3", - "phpstan/phpstan": "^1.1", - "phpstan/phpstan-phpunit": "^1.0", - "phpunit/phpunit": "^9.6", + "phpstan/phpstan": "^1.1 || ^2.0", + "phpstan/phpstan-deprecation-rules": "^1.0 || ^2.0", + "phpstan/phpstan-phpunit": "^1.0 || ^2.0", + "phpunit/phpunit": "^10.5", "squizlabs/php_codesniffer": "^3.7", "tecnickcom/tcpdf": "^6.5" }, @@ -2826,9 +2982,9 @@ ], "support": { "issues": "https://github.com/PHPOffice/PhpSpreadsheet/issues", - "source": "https://github.com/PHPOffice/PhpSpreadsheet/tree/2.1.0" + "source": "https://github.com/PHPOffice/PhpSpreadsheet/tree/5.1.0" }, - "time": "2024-05-11T04:17:56+00:00" + "time": "2025-09-04T05:34:49+00:00" }, { "name": "phpoption/phpoption", @@ -3621,6 +3777,60 @@ ], "time": "2024-04-27T21:32:50+00:00" }, + { + "name": "shuchkin/simplexlsx", + "version": "1.1.14", + "source": { + "type": "git", + "url": "https://github.com/shuchkin/simplexlsx.git", + "reference": "00006719a4d5df6426fa86c6586a5804941e8c38" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/shuchkin/simplexlsx/zipball/00006719a4d5df6426fa86c6586a5804941e8c38", + "reference": "00006719a4d5df6426fa86c6586a5804941e8c38", + "shasum": "" + }, + "require": { + "ext-libxml": "*", + "ext-mbstring": "*", + "ext-simplexml": "*", + "ext-zlib": "*", + "php": ">=5.5" + }, + "type": "library", + "autoload": { + "classmap": [ + "src/SimpleXLSX.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Sergey Shuchkin (SMSPILOT)", + "email": "sergey.shuchkin@gmail.com", + "homepage": "https://github.com/shuchkin" + } + ], + "description": "Parse and retrieve data from Excel XLSx files. MS Excel 2007 workbooks PHP reader.", + "homepage": "https://github.com/shuchkin/simplexlsx", + "keywords": [ + "backend", + "excel", + "parser", + "php", + "reader", + "xlsx" + ], + "support": { + "issues": "https://github.com/shuchkin/simplexlsx/issues", + "source": "https://github.com/shuchkin/simplexlsx/tree/1.1.14" + }, + "time": "2025-05-21T14:34:33+00:00" + }, { "name": "symfony/console", "version": "v6.4.7", @@ -9105,12 +9315,12 @@ ], "aliases": [], "minimum-stability": "stable", - "stability-flags": [], + "stability-flags": {}, "prefer-stable": true, "prefer-lowest": false, "platform": { "php": "^8.1" }, - "platform-dev": [], + "platform-dev": {}, "plugin-api-version": "2.6.0" } diff --git a/config/services.php b/config/services.php index a68f965..71c88a5 100644 --- a/config/services.php +++ b/config/services.php @@ -35,4 +35,8 @@ 'key' => env('IUCN_API_KEY'), 'endpoint' => env('IUCN_API_ENDPOINT'), ], + + 'mycobank' => [ + 'endpoint' => env('MYCOBANK_ENDPOINT'), + ], ]; diff --git a/database/migrations/2025_09_16_002032_create_failed_jobs_table.php b/database/migrations/2025_09_16_002032_create_failed_jobs_table.php new file mode 100644 index 0000000..249da81 --- /dev/null +++ b/database/migrations/2025_09_16_002032_create_failed_jobs_table.php @@ -0,0 +1,32 @@ +id(); + $table->string('uuid')->unique(); + $table->text('connection'); + $table->text('queue'); + $table->longText('payload'); + $table->longText('exception'); + $table->timestamp('failed_at')->useCurrent(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('failed_jobs'); + } +};