2026-05-20 18:25:15 +02:00
< ? php
namespace App\Services ;
use App\Exceptions\SubmissionException ;
use App\Helpers\EntryHelpers ;
use App\Http\Requests\StoreEntryRequest ;
2026-05-27 21:24:38 +02:00
use App\Jobs\CreateXenForoCommentsThread ;
2026-05-20 18:25:15 +02:00
use App\Models\Author ;
use App\Models\Entry ;
use App\Models\EntryFile ;
use App\Models\EntryGallery ;
use App\Models\EntryHash ;
use App\Models\Game ;
use App\Models\Genre ;
use App\Models\Language ;
use App\Models\Modification ;
use App\Models\Platform ;
use Illuminate\Support\Facades\Cache ;
use Illuminate\Support\Facades\DB ;
use Illuminate\Support\Facades\Storage ;
/**
* @ phpstan - import - type FSFileData from \App\Types\FSTypes
*/
class SubmissionsService {
/**
* Request for store / edit .
* @ var StoreEntryRequest | null
*/
private ? StoreEntryRequest $request = null ;
/**
* Section for store / edit .
* @ var string | null
*/
private ? string $section = null ;
2026-05-27 21:24:38 +02:00
/**
* Entry for edit .
* @ var Entry | null
*/
private ? Entry $entry = null ;
2026-05-20 18:25:15 +02:00
/**
* @ return list < FSFileData >
*/
public function prepareOldFiles ( ? Entry $entry = null ) : array
{
if ( $entry === null ){
$files = old ( 'files_uuid' , [] );
} else {
$files = old ( 'files_uuid' , $entry -> files -> pluck ( 'file_uuid' ) -> toArray () );
}
if ( $files === [] )
return [];
return array_map (
function ( string $uuid ) {
$file = EntryFile :: where ( 'file_uuid' , $uuid ) -> first ();
if ( $file )
return [
'name' => $file -> filename ,
'totalChunks' => 0 , // Already uploaded.
'rawFile' => null ,
'progressValue' => 0 ,
'currentChunk' => 0 ,
'done' => true ,
'error' => null ,
'uuid' => $uuid
];
$file = Cache :: get ( " uploaded_file_ { $uuid } " );
if ( $file )
return [
'name' => $file [ 'filename' ],
'totalChunks' => 0 , // Already uploaded.
'rawFile' => null ,
'progressValue' => 0 ,
'currentChunk' => 0 ,
'done' => true ,
'error' => null ,
'uuid' => $uuid
];
return null ;
},
$files );
}
/**
* @ param StoreEntryRequest $request
* @ param string $section
*
* @ return Entry
* @ throws SubmissionException
* @ throws \Throwable
*/
public function storeEntry ( StoreEntryRequest $request , string $section ){
// STEP 1 : Prepare basic fields.
$this -> request = $request ;
$this -> section = $section ;
$user_id = 0 ; // TODO: Replace that.
$entry = DB :: transaction ( function () use ( $user_id ) {
// STEP 2 : Create game.
$gameId = null ;
if ( section_must_be ( [ 'romhacks' , 'translations' ], $this -> section ) ){
$gameId = $this -> Step2_CreateAndReturnGameId ();
}
// STEP 3 : Create Complete title.
$completeTitle = $this -> Step3_BuildCompleteTitle ( $gameId );
// STEP 4 : Generate slug and entry title.
$entrySlug = EntryHelpers :: uniqueSlug ( $completeTitle , Entry :: class );
if ( section_must_be ( 'translations' , $this -> section ) &&
! $this -> request -> input ( 'entry_title' ) ){
$entryTitle = Game :: find ( $gameId ) -> name ;
} else {
$entryTitle = $this -> request -> input ( 'entry_title' );
}
// STEP 5 : Removed / Delayed.
// $mainImage = $this->Step5_MoveMainImage();
// STEP 6 : Prepare entry fields and save entry.
$fields = [
'type' => $this -> section ,
'title' => $entryTitle ,
'slug' => $entrySlug ,
'description' => $this -> request -> input ( 'description' ),
'main_image' => $this -> request -> input ( 'main-image' ),
'state' => $this -> request -> input ( 'submit-state' ),
'game_id' => $gameId ,
'status_id' => $this -> request -> input ( 'status' ),
'version' => $this -> request -> input ( 'version' ),
'release_date' => $this -> request -> input ( 'release-date' ),
'staff_credits' => $this -> request -> input ( 'staff_credits' ),
'relevant_link' => $this -> request -> input ( 'release_site' ),
'youtube_link' => $this -> request -> input ( 'youtube_video' ),
'user_id' => $user_id ,
'complete_title' => $completeTitle ,
];
$entry = Entry :: create ( $fields );
// STEP 7 : Save entry fields.
$this -> Step7_SaveEntryFiles ( $entry -> id );
// STEP 8 : Save hashes.
$this -> Step8_SaveHashes ( $entry -> id );
// STEP 9 : Save Authors.
$this -> Step9_SaveAuthors ( $entry );
// STEP 10 : Save Modifications.
if ( section_must_be ( 'romhacks' , $this -> section ) ){
$this -> Step10_SaveRomhacksModifications ( $entry );
}
// STEP 11 : Save Languages
$this -> Step11_SaveLanguages ( $entry );
// STEP 12 : Prepare Gallery images.
$this -> Step12a_PrepareGalleryImages ( $entry );
return $entry ;
});
// Step 12, Move main image and gallery.
$this -> Step12b_MoveMainImage ( $entry );
$this -> Step12c_SaveGalleryImages ( $entry );
2026-05-27 21:24:38 +02:00
// Step 13: Try to create the comments section.
$this -> Step13_CreateCommentsThread ( $entry );
2026-05-20 18:25:15 +02:00
return $entry ;
}
/**
* @ return int
*
* @ throws SubmissionException
*/
private function Step2_CreateAndReturnGameId () : int {
// Already existing game.
if ( $this -> request -> input ( 'game_id' ) )
return $this -> request -> input ( 'game_id' );
// Need to create a game.
2026-05-27 21:24:38 +02:00
$game = $this -> createGameFromFormFields ();
return $game -> id ;
}
private function createGameFromFormFields () : Game
{
2026-05-20 18:25:15 +02:00
if ( ! $this -> request -> input ( 'new-game-title' ) || ! $this -> request -> input ( 'new-game-platform' ) || ! $this -> request -> input ( 'new-game-genre' ) )
throw new SubmissionException ( " New game informations is missing " );
$platform = Platform :: find ( $this -> request -> input ( 'new-game-platform' ) );
$genre = Genre :: find ( $this -> request -> input ( 'new-game-genre' ) );
if ( ! $platform || ! $genre )
throw new SubmissionException ( " Incorrect game platform id " );
$gameSlug = EntryHelpers :: uniqueSlug ( $this -> request -> input ( 'new-game-title' ), Game :: class );
2026-05-27 21:24:38 +02:00
return Game :: create ([
2026-05-20 18:25:15 +02:00
'name' => trim ( $this -> request -> input ( 'new-game-title' ) ),
'slug' => $gameSlug ,
'platform_id' => $platform -> id ,
'genre_id' => $genre -> id ,
]);
}
/**
* Prepare and build complete title .
*
* @ param int | null $gameId
*
* @ return string
*/
private function Step3_BuildCompleteTitle ( ? int $gameId = null ) : string {
$fields = [];
$fields [ 'entry_title' ] = $this -> request -> input ( 'entry_title' ) ? ? null ;
if ( section_must_be ( [ 'homebrew' , 'translations' ], $this -> section ) && $gameId ){
$fields [ 'game_name' ] = Game :: find ( $gameId ) -> name ;
}
if ( section_must_be ( 'translations' , $this -> section ) ) {
$fields [ 'languages_string' ] = Language :: whereIn ( 'id' , $this -> request -> input ( 'languages' , [])) -> pluck ( 'name' ) -> implode ( ', ' );
}
2026-05-27 21:24:38 +02:00
if ( section_must_be ([ 'romhacks' , 'translations' , 'homebrew' , 'lua-scripts' , 'tutorials' ], $this -> section ) ) {
2026-05-20 18:25:15 +02:00
// TODO: Add single platform ID compatibility.
$fields [ 'platform_name' ] = Game :: find ( $gameId ) -> platform -> name ;
}
return EntryHelpers :: buildCompleteTitle ( $this -> section , $fields );
}
/**
* @ param int $entryId
*
* @ return void
* @ throws SubmissionException
*/
2026-05-27 21:24:38 +02:00
private function Step7_SaveEntryFiles ( int $entryId , ? array $uuidData = null ) : void
2026-05-20 18:25:15 +02:00
{
2026-05-27 21:24:38 +02:00
if ( ! $uuidData )
$uuidData = $this -> request -> input ( 'files_uuid' , [] );
foreach ( $uuidData as $uuid ) {
2026-05-20 18:25:15 +02:00
$fileData = Cache :: pull ( " uploaded_file_ { $uuid } " );
if ( ! $fileData )
2026-05-27 21:24:38 +02:00
throw new SubmissionException ( " File { $uuid } has expired. Please delete all your files and retry. If it's an edition, delete all your new files and retry. " );
2026-05-20 18:25:15 +02:00
EntryFile :: create ([
'entry_id' => $entryId ,
'file_uuid' => $uuid ,
'filename' => $fileData [ 'filename' ],
'filepath' => $fileData [ 'filepath' ],
'favorite_server' => $fileData [ 'favorite_server' ],
'favorite_at' => \DateTimeImmutable :: createFromTimestamp ( $fileData [ 'favorite_at' ] ),
'filesize' => $fileData [ 'filesize' ],
'state' => 'public'
]);
}
}
/**
* @ param int $entryId
*
* @ return void
*/
private function Step8_SaveHashes ( int $entryId ) : void
{
foreach ( $this -> request -> input ( 'hashes' , [] ) as $hash ) {
if ( ! isset ( $hash [ 'filename' ], $hash [ 'hash_crc32' ], $hash [ 'hash_sha1' ], $hash [ 'verified' ]) ) {
continue ;
}
EntryHash :: create ([
'entry_id' => $entryId ,
'filename' => $hash [ 'filename' ],
'hash_crc32' => $hash [ 'hash_crc32' ],
'hash_sha1' => $hash [ 'hash_sha1' ],
'verified' => $hash [ 'verified' ],
]);
}
}
/**
* @ param Entry $entry
*
* @ return void
* @ throws SubmissionException
*/
private function Step9_SaveAuthors ( Entry $entry ) : void
{
2026-05-27 21:24:38 +02:00
// TODO: Code fragment to be replaced by edit version.
2026-05-20 18:25:15 +02:00
// Existing authors.
foreach ( $this -> request -> input ( 'authors' , [] ) as $authorId ) {
$author = Author :: find ( $authorId );
if ( ! $author )
throw new SubmissionException ( " Author { $authorId } does not exist. " );
$entry -> authors () -> attach ( $author -> id );
}
// New Authors
foreach ( $this -> request -> input ( 'new-authors' , [] ) as $authorName ) {
$authorName = trim ( $authorName );
if ( $authorName === '' )
continue ;
$author = Author :: firstOrCreate (
[ 'slug' => EntryHelpers :: uniqueSlug ( $authorName , Author :: class )],
[ 'name' => $authorName ]
);
$entry -> authors () -> attach ( $author -> id );
}
}
/**
* @ param Entry $entry
*
* @ return void
* @ throws SubmissionException
*/
private function Step10_SaveRomhacksModifications ( Entry $entry ) : void
{
2026-05-27 21:24:38 +02:00
// TODO: Replace by edit version
2026-05-20 18:25:15 +02:00
foreach ( $this -> request -> input ( 'modifications' , [] ) as $modificationId ) {
$modification = Modification :: find ( $modificationId );
if ( ! $modification )
throw new SubmissionException ( " Modification { $modificationId } does not exist. " );
$entry -> modifications () -> attach ( $modification -> id );
}
}
/**
* @ param Entry $entry
*
* @ return void
* @ throws SubmissionException
*/
private function Step11_SaveLanguages ( Entry $entry ) : void
{
2026-05-27 21:24:38 +02:00
// TODO: Replace by edit version.
2026-05-20 18:25:15 +02:00
foreach ( $this -> request -> input ( 'languages' , [] ) as $languageId ) {
$language = Language :: find ( $languageId );
if ( ! $language )
throw new SubmissionException ( " Language { $languageId } does not exist. " );
$entry -> languages () -> attach ( $language -> id );
}
}
private function Step12a_PrepareGalleryImages ( Entry $entry ) : void
{
foreach ( $this -> request -> input ( 'gallery' , [] ) as $imagePath ) {
EntryGallery :: create ([
'entry_id' => $entry -> id ,
'image' => $imagePath ,
]);
}
}
/**
* @ param Entry $entry
*
* @ return void
*/
private function Step12b_MoveMainImage ( Entry $entry ) : void {
$mainImage = $entry -> main_image ;
$newPath = 'entries/main-images/' . basename ( $mainImage );
if ( ! Storage :: disk ( 'public' ) -> move ( $mainImage , $newPath ) )
return ;
$entry -> update ([ 'main_image' => $newPath ]);
}
private function Step12c_SaveGalleryImages ( Entry $entry ) : void
{
foreach ( $entry -> gallery as $galleryItem ) {
$newPath = 'entries/gallery-images/' . $entry -> id . '/' . basename ( $galleryItem -> image );
if ( ! Storage :: disk ( 'public' ) -> move ( $galleryItem -> image , $newPath ) )
continue ;
$galleryItem -> update ([ 'image' => $newPath ]);
}
}
2026-05-27 21:24:38 +02:00
public function editEntry ( StoreEntryRequest $request , string $section , Entry $entry ) : Entry
{
// STEP 1: Prepare basic fields and keep in save some others fields.
$this -> request = $request ;
$this -> section = $section ;
$this -> entry = $entry ;
$user_id = 0 ; // TODO: Replace that.
$oldMainImage = $entry -> main_image ;
$galleryPaths = [];
$entry = DB :: transaction ( function () use ( $user_id , & $galleryPaths ){
// STEP 2: Create game if different.
$gameId = null ;
if ( section_must_be ( [ 'romhacks' , 'translations' ], $this -> section ) ){
$gameId = $this -> eStep2_VerifyCreateAndEditGameId ();
}
// STEP 3: Recreate complete title and refresh slug if needed.
$completeTitle = $this -> Step3_BuildCompleteTitle ( $gameId );
if ( $completeTitle !== $this -> entry -> complete_title ) {
$this -> entry -> complete_title = $completeTitle ;
$this -> entry -> slug = EntryHelpers :: uniqueSlug ( $completeTitle , Entry :: class , $this -> entry -> id );
}
// STEP 4: Regenerate entry title.
if ( section_must_be ( 'translations' , $this -> section ) &&
! $this -> request -> input ( 'entry_title' ) ){
$this -> entry -> title = Game :: find ( $gameId ) -> name ;
} else {
$this -> entry -> title = $this -> request -> input ( 'entry_title' );
}
// STEP 5: Update entry fields.
$fields = [
'type' => $this -> section ,
'title' => $this -> entry -> title , // Useless, I know.
'slug' => $this -> entry -> slug ,
'description' => $this -> request -> input ( 'description' ),
'main_image' => $this -> request -> input ( 'main-image' ),
'state' => $this -> request -> input ( 'submit-state' ),
'game_id' => $gameId ,
'status_id' => $this -> request -> input ( 'status' ),
'version' => $this -> request -> input ( 'version' ),
'release_date' => $this -> request -> input ( 'release-date' ),
'staff_credits' => $this -> request -> input ( 'staff_credits' ),
'relevant_link' => $this -> request -> input ( 'release_site' ),
'youtube_link' => $this -> request -> input ( 'youtube_video' ),
'user_id' => $user_id ,
'complete_title' => $completeTitle ,
];
$this -> entry -> update ( $fields );
// STEP 6: Update entry files.
$this -> eStep6_UpdateEntryFiles ( $this -> entry -> id );
// STEP 7: Update hashes.
$this -> eStep7_UpdateHashes ( $this -> entry -> id );
// STEP 8: Update Authors.
$this -> eStep8_UpdateAuthors ();
// STEP 9: Update romhacks modifications.
if ( section_must_be ( 'romhacks' , $this -> section ) ) {
$this -> eStep9_UpdateRomhacksModifications ();
}
// STEP 10: Update Languages.
$this -> eStep10_UpdateLanguages ();
// STEP 11: Prepare new gallery images and prepare deletion of others ones.
$galleryPaths = $this -> eStep11a_UpdateGalleryImages ();
// STEP 13: Try to create comments area if it doesn't exist.
$this -> Step13_CreateCommentsThread ( $this -> entry );
return $this -> entry ;
});
// STEP 11 : Update main image if needed.
$this -> eStep11b_UpdateMainImage ( $oldMainImage );
// STEP 11 : Update gallery storage.
$this -> eStep11c_UpdateGalleryImages ( $galleryPaths );
return $entry ;
}
/**
* @ throws SubmissionException
*/
private function eStep2_VerifyCreateAndEditGameId () : int
{
// Already existing game.
if ( $this -> request -> input ( 'game_id' ) ){
if ( $this -> entry -> game_id == $this -> request -> input ( 'game_id' ) ){
return $this -> entry -> game_id ; // No changes.
} else { // Change in game but already exist.
$game = Game :: find ( $this -> request -> input ( 'game_id' ) );
if ( ! $game )
throw new SubmissionException ( " Game { $this -> request -> input ( 'game_id' ) } does not exist. " );
$this -> entry -> game_id = $game -> id ;
return $this -> entry -> game_id ;
}
}
// Need to create a game.
$game = $this -> createGameFromFormFields ();
$this -> entry -> game_id = $game -> id ;
return $this -> entry -> game_id ;
}
/**
* @ throws SubmissionException
*/
private function eStep6_UpdateEntryFiles ( int $entryId ) : void
{
$requestUuids = $this -> request -> input ( 'files_uuid' , []);
$existingUuids = EntryFile :: where ( 'entry_id' , $entryId ) -> pluck ( 'file_uuid' ) -> toArray ();
$needDeletion = array_diff ( $existingUuids , $requestUuids );
if ( ! empty ( $needDeletion ) ){
EntryFile :: where ( 'entry_id' , $entryId ) -> whereIn ( 'file_uuid' , $needDeletion ) -> delete ();
}
$needAddition = array_diff ( $requestUuids , $existingUuids );
if ( ! empty ( $needAddition ) ){
$this -> Step7_SaveEntryFiles ( $this -> entry -> id , $needAddition ); // Same code.
}
}
private function eStep7_UpdateHashes ( int $entryId ) : void
{
$requestHashes = collect ( $this -> request -> input ( 'hashes' , [] ) )
-> filter ( fn ( $h ) => isset ( $h [ 'filename' ], $h [ 'hash_crc32' ], $h [ 'hash_sha1' ], $h [ 'verified' ] ) )
-> keyBy ( 'hash_sha1' )
-> toArray ();
;
$existingHashes = EntryHash :: where ( 'entry_id' , $entryId ) -> get () -> keyBy ( 'hash_sha1' );
$hashsToDelete = array_diff ( $existingHashes -> keys () -> toArray (), array_keys ( $requestHashes ) );
if ( ! empty ( $hashsToDelete ) ){
EntryHash :: where ( 'entry_id' , $entryId ) -> whereIn ( 'hash_sha1' , $hashsToDelete ) -> delete ();
}
foreach ( $requestHashes as $sha1 => $hash ){
if ( $existingHashes -> has ( $sha1 ) ){
$existingHashes -> get ( $sha1 ) -> update ([
'filename' => $hash [ 'filename' ],
'hash_crc32' => $hash [ 'hash_crc32' ],
'hash_sha1' => $hash [ 'hash_sha1' ],
'verified' => $hash [ 'verified' ],
]);
} else {
EntryHash :: create ([
'entry_id' => $entryId ,
'filename' => $hash [ 'filename' ],
'hash_crc32' => $hash [ 'hash_crc32' ],
'hash_sha1' => $hash [ 'hash_sha1' ],
'verified' => $hash [ 'verified' ],
]);
}
}
}
/**
* @ return void
* @ throws SubmissionException
*/
private function eStep8_UpdateAuthors () : void
{
$syncAuthorsId = [];
$requestAuthorsId = $this -> request -> input ( 'authors' , [] );
if ( ! empty ( $requestAuthorsId ) ){
$valid = Author :: whereIn ( 'id' , $requestAuthorsId ) -> pluck ( 'id' ) -> toArray ();
if ( count ( $valid ) !== count ( $requestAuthorsId ) ){
throw new SubmissionException ( " One of the authors doesn't exist. " );
}
$syncAuthorsId = array_merge ( $syncAuthorsId , $requestAuthorsId );
}
foreach ( $this -> request -> input ( 'new-authors' , [] ) as $authorName ) {
$authorName = trim ( $authorName );
if ( $authorName === '' )
continue ;
$author = Author :: firstOrCreate (
[ 'slug' => EntryHelpers :: uniqueSlug ( $authorName , Author :: class )],
[ 'name' => $authorName ]
);
$syncAuthorsId [] = $author -> id ;
}
$this -> entry -> authors () -> sync ( $syncAuthorsId );
}
/**
* @ return void
* @ throws SubmissionException
*/
private function eStep9_UpdateRomhacksModifications () : void
{
$requestModifications = $this -> request -> input ( 'modifications' , [] );
if ( ! empty ( $requestModifications ) ){
$valid = Modification :: whereIn ( 'id' , $requestModifications ) -> pluck ( 'id' ) -> toArray ();
if ( count ( $valid ) !== count ( $requestModifications ) ){
throw new SubmissionException ( " One of the modifications doesn't exist. " );
}
}
$this -> entry -> modifications () -> sync ( $requestModifications );
}
/**
* @ return void
* @ throws SubmissionException
*/
private function eStep10_UpdateLanguages () : void
{
$requestLanguages = $this -> request -> input ( 'languages' , [] );
if ( ! empty ( $requestLanguages ) ){
$valid = Language :: whereIn ( 'id' , $requestLanguages ) -> pluck ( 'id' ) -> toArray ();
if ( count ( $valid ) !== count ( $requestLanguages ) ){
throw new SubmissionException ( " One of the languages doesn't exist. " );
}
}
$this -> entry -> languages () -> sync ( $requestLanguages );
}
private function eStep11a_UpdateGalleryImages () : array
{
$requestGallery = $this -> request -> input ( 'gallery' , [] );
$existingGalleryPaths = $this -> entry -> gallery -> pluck ( 'image' ) -> toArray ();
$needDeletion = array_diff ( $existingGalleryPaths , $requestGallery );
if ( ! empty ( $needDeletion ) ){
EntryGallery :: where ( 'entry_id' , $this -> entry -> id ) -> whereIn ( 'image' , $needDeletion ) -> delete ();
}
$needAddition = array_diff ( $requestGallery , $existingGalleryPaths );
$images = [];
foreach ( $needAddition as $imagePath ){
$images [] = EntryGallery :: create ([
'entry_id' => $this -> entry -> id ,
'image' => $imagePath ,
]);
}
return [ 'addition' => $images , 'deletion' => $needDeletion ];
}
private function eStep11b_UpdateMainImage ( ? string $oldMainImagePath ) : void
{
$currentMainImagePath = $this -> entry -> main_image ;
if ( $currentMainImagePath === $oldMainImagePath )
return ;
$newPath = 'entries/main-images/' . basename ( $currentMainImagePath );
if ( ! Storage :: disk ( 'public' ) -> move ( $currentMainImagePath , $newPath ) ){
$this -> entry -> update ([ 'main_image' => $oldMainImagePath ]);
return ;
}
$this -> entry -> update ([ 'main_image' => $newPath ]);
if ( $oldMainImagePath && Storage :: disk ( 'public' ) -> exists ( $oldMainImagePath ) )
Storage :: disk ( 'public' ) -> delete ( $oldMainImagePath );
}
private function eStep11c_UpdateGalleryImages ( array $pathsChanges ) : void
{
foreach ( $pathsChanges [ 'deletion' ] as $deletePath ){
if ( Storage :: disk ( 'public' ) -> exists ( $deletePath ) )
Storage :: disk ( 'public' ) -> delete ( $deletePath );
}
foreach ( $pathsChanges [ 'addition' ] as $galleryItem ){
$newPath = 'entries/gallery-images/' . $this -> entry -> id . '/' . basename ( $galleryItem -> image );
if ( ! Storage :: disk ( 'public' ) -> move ( $galleryItem -> image , $newPath ) ){
continue ;
}
$galleryItem -> update ([ 'image' => $newPath ]);
}
}
private function Step13_CreateCommentsThread ( Entry $entry ) : void
{
if ( ! $entry -> comments_thread_id )
CreateXenForoCommentsThread :: dispatch ( $entry );
// app(XenforoApiService::class)->createCommentsThread( $entry );
}
2026-05-20 18:25:15 +02:00
}