HEX
Server: Microsoft-IIS/8.5
System: Windows NT YDAWBH120 6.3 build 9600 (Windows Server 2012 R2 Standard Edition) AMD64
User: tentjecom_web (0)
PHP: 7.4.14
Disabled: NONE
Upload Files
File: D:/HostingSpaces/ZelfVerkopen/zelfverkopen.nl/app/KommaApp/Realworks/Kms/RealWorksService.php
<?php

namespace App\KommaApp\Realworks\Kms;

use App\KommaApp\Kms\Core\Sections\SectionService;
use App\KommaApp\Languages\Models\Language;
use App\KommaApp\Pages\Models\Page;
use App\KommaApp\Realworks\Models\Huur;
use App\KommaApp\Realworks\Models\HuurTranslation;
use App\KommaApp\Realworks\Models\Koop;
use App\KommaApp\Realworks\Models\KoopTranslation;
use App\KommaApp\Realworks\Models\ObjectDetails;
use App\KommaApp\Realworks\Models\ObjectDetailsTranslation;
use App\KommaApp\Realworks\Models\RealworkObject;
use App\Mail\RealworksFailMail;
use Carbon\Carbon;
use Illuminate\Database\Eloquent\Collection;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Facades\Log;
use ZipArchive;

class RealWorksService extends SectionService
{
    protected $sortable = true;

    protected static $DOWNLOAD_FOLDER_NAME = 'download'; //without trailing slash
    protected static $XML_FILE_NAME = 'voorbeeld.wonen'; //without extension

    public function __construct()
    {
        $this->forModelName = Page::class; //TODO Change me

        parent::__construct();
    }

    /**
     * Downloads the zipped xml from Realworks.
     *
     * Warning. Strict regulations by Realworks apply. Please check their documentation for the latest info on those
     * regulations. At the time of writing they are:
     *
     * - Only call the api once a day after 08:30 in the morning.
     * - The media that is defined in the xml must be downloaded for displaying.
     */
    public function DownloadZipWithXML()
    {
        $storageFolder = $this->createAndGetRealworksStorageFolder(self::$DOWNLOAD_FOLDER_NAME);

        $url = $this->getUrl();

        try {
            $file = file_get_contents($url);
        }
        catch (\Exception $exception)
        {
            $this->mailOrShowErrorAndDie('Could not save downloaded realworks zip');
        }


        $zipPath = $storageFolder . config('realworks.zip_name', 'realworks') . '.zip';
        if(file_put_contents($zipPath, $file) === false) $this->mailOrShowErrorAndDie('Could not save downloaded realworks zip ("' . $zipPath . '")');
    }

    /**
     * Extracts the realworks zip and deletes the zip file
     */
    public function ProcessZip()
    {
        $storageFolder = $this->createAndGetRealworksStorageFolder(self::$DOWNLOAD_FOLDER_NAME);
        $zipPath = $storageFolder . config('realworks.zip_name', 'realworks') . '.zip';

        $zip = new ZipArchive;
        if ($zip->open($zipPath) === true) {
            $zip->extractTo($storageFolder);
            $zip->close();

            if(unlink($zipPath) === false) $this->mailOrShowErrorAndDie('Could delete realworks zip file after extraction: ' . $zipPath);

        } else {
            $this->mailOrShowErrorAndDie('Could not extract realworks zip file: ' . $zipPath);
        }
    }

    /**
     * Clears the storage folder in which the downloaded zip and its extracted data is placed
     */
    public function ClearDownloadFolder()
    {
        self::rrmdir($this->createAndGetRealworksStorageFolder(self::$DOWNLOAD_FOLDER_NAME), false);
    }

    /**
     * MUST be triggered by App\Console\Kernel to update Realworks api stuff.
     */
    public function executeSchedule()
    {
        $this->ClearDownloadFolder();
        $this->DownloadZipWithXML(); //WARNING! Only runnable once a day. Else we will get fined!
        $this->ProcessZip();
        $xml = $this->DownloadedXMLToArray();
        $this->DownloadMediaFromXMLToPublicFolders($xml); //WARNING! Only runnable once a day. Else we will get fined!
        $this->storeObjectsInDatabase($xml);
        $this->deleteObjectsFromDatabase($xml);
        $this->deletePublicFoldersFromNonExistingObjects();
    }

    /**
     * Stores the properties in the database by using the xml
     *
     * @param array $xml
     */
    public function storeObjectsInDatabase(array $xml)
    {
        if(! array_key_exists('Object', $xml)) return; //Empty

        foreach($xml['Object'] as $objectArray)
        {
            if(! $this->hasValidMainPhoto($objectArray)) {
                $objectId = $objectArray['ObjectSystemID'] ?? 'unknown';
                Log::error('Main photo missing or invalid. Skipping object: ' . $objectId);
                continue;
            }

            $object = $this->createObjectFromXML($objectArray);
        }
    }

    /**
     * Creates an object model from the input xml data
     *
     * @param $xml
     * @return RealworkObject|null
     */
    private function createObjectFromXML(array $xml): ?RealworkObject
    {
        if(! isset($xml['ObjectSystemID'])) $this->mailOrShowErrorAndDie('An object did not have the required ObjectSystemID key. Stopped processing');
        $dutchLanguage = Language::where('native_name', '=', 'Nederlands')->first();
        if(! $dutchLanguage) $this->mailOrShowErrorAndDie('The dutch language did not exists. The object details could not be created. Stopped processing');

        $object = RealworkObject::where('system_id', '=', $xml['ObjectSystemID'])->first();
        if(! $object) $object = new RealworkObject();

        $object->raw_json = json_encode($xml);
        if(isset($xml['NVMVestigingNR'])) $object->nvm_vestiging_nummer = $xml['NVMVestigingNR'];
        if(isset($xml['ObjectCompany'])) $object->company = $xml['ObjectCompany'];
        if(isset($xml['ObjectAfdeling'])) $object->afdeling = $xml['ObjectAfdeling'];
        if(isset($xml['ObjectTiaraID'])) $object->tiara_id = $xml['ObjectTiaraID'];
        if(isset($xml['ObjectSystemID'])) $object->system_id = $xml['ObjectSystemID'];
        if(isset($xml['ObjectCode'])) $object->code = $xml['ObjectCode'];
        if(! isset($xml['ObjectDetails'])) $this->mailOrShowErrorAndDie('An object did not contain object details. Stopped processing');
        $object->save();

        /** @var ObjectDetailsTranslation $objectDetailsTranslation */
        $objectDetailsTranslation = $this->createObjectDetailsFromXML($xml['ObjectDetails'], $object, $dutchLanguage)->translations->first();
        $object->public_path = $this->getPublicFolderPathForObject($object->system_id, $objectDetailsTranslation->woonplaats, $objectDetailsTranslation->straatnaam, $objectDetailsTranslation->huisnummer);
        $object->public_path = str_replace(public_path(), '', $object->public_path);
        $object->public_path = str_replace('\\', '/', $object->public_path);

        $object->save();

        return $object;
    }

    /**
     * Creates an object details model from the input xml data
     *
     * @param array $xml
     * @param RealworkObject $object
     * @param Language $language
     * @return ObjectDetails
     */
    private function createObjectDetailsFromXML(array $xml, RealworkObject $object, Language $language): ObjectDetails {
        /** @var ObjectDetails $objectDetails */
        $objectDetails = $object->objectDetails()->first();

        if($objectDetails) {
            //Check if the object was updated yesterday. If not we are going to skip it
            $updateDate = Carbon::createFromFormat('Y-m-d', $xml['DatumWijziging']);
            $now = Carbon::now();
            $now = Carbon::create($now->year, $now->month, $now->day);
            $needsUpdate = ($updateDate && $now->subDay()->equalTo($updateDate)) ? true : false;
            if(! $needsUpdate) return $objectDetails;
        }

        if(! $objectDetails) {
            $objectDetails = new ObjectDetails();
            $objectDetails->object()->associate($object);
            $objectDetails->save();
        }

        $objectDetailsTranslation = $objectDetails->translations()->where('language_id', '=', $language->id)->first();
        if(! $objectDetailsTranslation) {
            $objectDetailsTranslation = new ObjectDetailsTranslation();
            $objectDetailsTranslation->translatable()->associate($objectDetails);
            $objectDetailsTranslation->language()->associate($language);
        }

        if(isset($xml['Adres']) && isset($xml['Adres']['Nederlands'])) {
            $adresData = $xml['Adres']['Nederlands'];

            if (isset($adresData['Straatnaam'])) $objectDetailsTranslation->straatnaam = $adresData['Straatnaam'];
            if (isset($adresData['Huisnummer'])) $objectDetailsTranslation->huisnummer = $adresData['Huisnummer'];
            if (isset($adresData['HuisnummerToevoeging'])) $objectDetailsTranslation->huisnummer_toevoeging = $adresData['HuisnummerToevoeging'];
            if (isset($adresData['Postcode'])) $objectDetailsTranslation->postcode = $adresData['Postcode'];
            if (isset($adresData['Woonplaats'])) $objectDetailsTranslation->woonplaats = $adresData['Woonplaats'];
            if (isset($adresData['Land'])) $objectDetailsTranslation->land = $adresData['Land'];
        }
        if (isset($xml['Aanvaarding'])) $objectDetailsTranslation->aanvaarding = $xml['Aanvaarding'];
        if (isset($xml['DatumAanvaarding'])) $objectDetailsTranslation->toelichting_aanvaarding = $xml['DatumAanvaarding'];
        if (isset($xml['ToelichtingAanvaarding'])) $objectDetailsTranslation->toelichting_aanvaarding = $xml['ToelichtingAanvaarding'];
        if (isset($xml['DatumAanvaarding'])) $this->updateYearMonthDate($objectDetailsTranslation, 'datum_aanvaarding', $xml['DatumAanvaarding']);
        if (isset($xml['ObjectAanmelding'])) $objectDetailsTranslation->object_aanmelding = $xml['ObjectAanmelding'];
        if (isset($xml['DatumInvoer'])) $this->updateYearMonthDate($objectDetailsTranslation, 'datum_invoer', $xml['DatumInvoer']);
        if (isset($xml['DatumWijziging'])) $this->updateYearMonthDate($objectDetailsTranslation, 'datum_wijziging', $xml['DatumWijziging']);
        if (isset($xml['DatumVeiling'])) $this->updateYearMonthDate($objectDetailsTranslation, 'datum_veiling', $xml['DatumVeiling']);
        if (isset($xml['StatusBeschikbaarheid'])) {
            $statusBeschikbaarheid = $xml['StatusBeschikbaarheid'];
            if (isset($statusBeschikbaarheid['Status'])) $objectDetailsTranslation->status_beschikbaarheid = $statusBeschikbaarheid['Status'];
            if (isset($statusBeschikbaarheid['VerkochtOnderVoorbehoud'])) {
                $verkochtOnderVoorbehoud = $statusBeschikbaarheid['VerkochtOnderVoorbehoud'];
                if(isset($verkochtOnderVoorbehoud['Datum'])) $this->updateYearMonthDate($objectDetailsTranslation, 'status_verkocht_onder_voorbehoud_datum', $verkochtOnderVoorbehoud['Datum']);
                if(isset($verkochtOnderVoorbehoud['DatumVoorbehoudTot'])) $this->updateYearMonthDate($objectDetailsTranslation, 'status_verkocht_onder_voorbehoud_datum_tot', $verkochtOnderVoorbehoud['DatumVoorbehoudTot']);
            }
            if (isset($statusBeschikbaarheid['TransactieDatum'])) $this->updateYearMonthDate($objectDetailsTranslation, 'status_transactie_datum', $statusBeschikbaarheid['TransactieDatum']);
        }
        if (isset($xml['Bouwvorm'])) $objectDetailsTranslation->bouwvorm = $xml['Bouwvorm'];
        if (isset($xml['Aanbiedingstekst'])) $objectDetailsTranslation->aanbiedingstekst = $xml['Aanbiedingstekst'];

        $objectDetailsTranslation->save();

        if(isset($xml['Koop'])) $this->createKoopFromXML($xml['Koop'], $objectDetails, $language);
        if(isset($xml['Huur'])) $this->createHuurFromXml($xml['Huur'], $objectDetails, $language);

        return $objectDetails;
    }

    private function createKoopFromXML(array $xml, ObjectDetails $objectDetails, Language $language): Koop
    {
        $koop = $objectDetails->koop()->first();
        if(! $koop) {
            $koop = new Koop();
            $koop->objectDetails()->associate($objectDetails);
            $koop->save();
        }

        $koopTranslation = $koop->translations()->where('language_id', '=', $language->id)->first();
        if(! $koopTranslation) {
            $koopTranslation = new KoopTranslation();
            $koopTranslation->translatable()->associate($koop);
            $koopTranslation->language()->associate($language);
        }

        if (isset($xml['Prijsvoorvoegsel'])) $koopTranslation->prijs_voorvoegsel = $xml['Prijsvoorvoegsel'];
        if (isset($xml['Koopprijs'])) $koopTranslation->koop_prijs = $xml['Koopprijs'];
        if (isset($xml['KoopConditie'])) $koopTranslation->koop_conditie = $xml['KoopConditie'];
        if (isset($xml['KoopSpecificatie'])) $koopTranslation->koop_specificatie = $xml['KoopSpecificatie'];
        if (isset($xml['WOZ'])) {
            $woz = $xml['WOZ'];
            if (isset($woz['WOZWaarde'])) $koopTranslation->woz_waarde = $woz['WOZWaarde'];
            if (isset($woz['WOZWaardePeildatum'])) $koopTranslation->woz_waarde_peildatum = Carbon::createFromFormat('Y-m-d', $woz['WOZWaardePeildatum']);
        }
        $koopTranslation->save();

        return $koop;
    }

    private function createHuurFromXml(array $xml, ObjectDetails $objectDetails, Language $language): Huur
    {
        $huur = $objectDetails->huur()->first();
        if(! $huur) {
            $huur = new Huur();
            $huur->objectDetails()->associate($objectDetails);
            $huur->save();
        }

        $huurTranslation = $huur->translations()->where('language_id', '=', $language->id)->first();
        if(! $huurTranslation) {
            $huurTranslation = new HuurTranslation();
            $huurTranslation->translatable()->associate($huur);
            $huurTranslation->language()->associate($language);
        }

        if (isset($xml['Huurprijs'])) $huurTranslation->huur_prijs = $xml['Huurprijs'];
        if (isset($xml['HuurConditie'])) $huurTranslation->huur_conditie = $xml['HuurConditie'];
        if (isset($xml['HuurSpecificatie'])) $huurTranslation->huur_specificatie = $xml['HuurSpecificatie'];
        $huurTranslation->save();

        return $huur;
    }

    /**
     * Parses the XML into an PHP array that we can use for further processing
     *
     * @return array
     */
    public function DownloadedXMLToArray():array
    {
        $storageFolder = $this->createAndGetRealworksStorageFolder(self::$DOWNLOAD_FOLDER_NAME);
        $exampleFileName = self::$XML_FILE_NAME . '.xml';
        $realFileName = $this->getTodaysXMLFileName();
        $fileName = (file_exists($storageFolder . DIRECTORY_SEPARATOR . $realFileName)) ? $realFileName : $exampleFileName;

        $xmlFile = file_get_contents($storageFolder . DIRECTORY_SEPARATOR . $fileName);
        $xml = simplexml_load_string($xmlFile, 'SimpleXMLElement', LIBXML_NOCDATA);
        $json = json_encode($xml);
        $array = json_decode($json, true);

//        Log::debug($array);

        return $array;
    }

    private function getTodaysXMLFileName():string
    {
        return 'WONEN_' . date('Y') . date('m') . date('d') . '.xml';
    }

    /**
     * Downloads all media from the given xml and passes it into a public folder
     * @param array $xml
     */
    private function DownloadMediaFromXMLToPublicFolders(array $xml)
    {
        $this->createAndGetPublicRealworksFolder();

        if(! array_key_exists('Object', $xml)) return; //Empty

        foreach($xml['Object'] as $objectArray)
        {
            if(! $this->hasValidMainPhoto($objectArray)) {
                $objectId = $objectArray['ObjectSystemID'] ?? 'unknown';
                Log::error('Main photo missing or invalid. Skipping object: ' . $objectId);
                continue;
            }

            $this->downloadMediaListIfNotAlreadyFromObjectAndReturnMediaArray($objectArray);
        }
    }

    /**
     * @param string $objectSystemId
     * @param string $place
     * @param string $street
     * @param string $houseNumber
     * @return string
     */
    public function getPublicFolderPathForObject(string $objectSystemId, string $place, string $street, string $houseNumber)
    {

        $folder = \Str::slug($place . '-' . $street . '-' . $houseNumber . '-' . $objectSystemId);
        $rootFolder = $this->createAndGetPublicRealworksFolder($folder);

        return $rootFolder;
    }

    /**
     * Downloads every file from the MediaLijst array inside the given object and returns the MediaLijst array after that
     *
     * @param array $object
     * @return array|mixed
     */
    public function downloadMediaListIfNotAlreadyFromObjectAndReturnMediaArray(&$object = ['plainId' => 0, 'id' => 0, 'MediaLijst' => null], $id = 0)
    {
        $mediaReturnArray = [];

        if(isset($object['MediaLijst']) && isset($object['ObjectSystemID']) && isset($object['ObjectDetails']))
        {
            //Validation
            if(! is_array($object['MediaLijst'])) $this->mailOnError('At least one MediaLijst entry in the Realworks xml wasn\'t an array. Needs direct fix');
            if(! is_array($object['ObjectDetails'])) $this->mailOnError('At least one ObjectDetails entry in the Realworks xml wasn\'t an array. Needs direct fix');
            if(! is_string($object['ObjectSystemID'])) $this->mailOnError('At least one ObjectSystemID entry in the Realworks xml wasn\'t a string. Needs direct fix');

            if(! isset($object['ObjectDetails']['Adres']) || ! isset($object['ObjectDetails']['Adres']['Nederlands']))
                $this->mailOnError('At least one ObjectDetails entry in the Realworks xml wasn\'t properly formatted. Expecting keys ObjectDetails > Adres > Nederlands. Needs direct fix');

            $addressArray = $object['ObjectDetails']['Adres']['Nederlands'];
            if(! is_array($addressArray)) $this->mailOnError('At least one ObjectDetails > Adres > Nederlands entry in the Realworks xml wasn\'t an array. Needs direct fix');

            if(count(array_diff(['Straatnaam', 'Huisnummer', 'Postcode', 'Woonplaats', 'Land'], array_keys($addressArray))) > 0)
                $this->mailOnError("At least one ObjectDetails > Adres > Nederlands entry in the Realworks xml did not contain all keys: 'Straatnaam', 'Huisnummer', 'Postcode', 'Woonplaats', 'Land'. Needs direct fix " . json_encode($addressArray));

            // Normaliseer MediaLijst → altijd array van media arrays
            if (! isset($object['MediaLijst']) || empty($object['MediaLijst'])) {
                $object['MediaLijst'] = [];
            }

            // Als MediaLijst geen array is, maak er één van
            if (! is_array($object['MediaLijst'])) {
                $object['MediaLijst'] = [$object['MediaLijst']];
            }

            // Loop door elke media groep
            foreach ($object['MediaLijst'] as $index => &$mediaGroup) {

                // Als mediaGroup geen array is → enkelvoudig item
                if (! is_array($mediaGroup)) {
                    $mediaGroup = [$mediaGroup];
                }

                // Loop door elke individuele media entry
                foreach ($mediaGroup as $i => &$mediaItem) {

                    // Wanneer mediaItem een string is → converteren naar array
                    // Voorbeeld: "HoofdFoto" wordt ["Groep" => "HoofdFoto"]
                    if (! is_array($mediaItem)) {
                        $mediaItem = ['Groep' => $mediaItem];
                    }
                }
            }

            foreach($object['MediaLijst'] as $index => $mediaArrays) {
                foreach($mediaArrays as $mediaArray)
                {
                    if(! isset($mediaArray['Groep'])) $this->mailOnError("At least one MediaLijst item did not have 'Groep' set. " . json_encode($mediaArray));

                    if (
                        ! isset($mediaArray['URL']) ||
                        empty($mediaArray['URL']) ||
                        ! is_string($mediaArray['URL'])
                    ) {
                        if(isset($mediaArray['Groep']) && $mediaArray['Groep'] === 'HoofdFoto') {
                            $objectId = $object['ObjectSystemID'] ?? 'unknown';
                            Log::error('Main photo missing or invalid. Skipping object: ' . $objectId);
                            return [];
                        }

                        $this->mailOnError('Media item missing or invalid URL field: ' . json_encode($mediaArray));
                        continue;
                    }

                    $propertyFolder = $this->getPublicFolderPathForObject($object['ObjectSystemID'], $addressArray['Woonplaats'], $addressArray['Straatnaam'], $addressArray['Huisnummer']);
                    $groupFolder = self::createFolderIfNotExitsOrFail($propertyFolder . DIRECTORY_SEPARATOR . $mediaArray['Groep']);

                    $url = trim($mediaArray['URL']);
                    $filenameWithExtension = self::getFilenameFromUrl($url);
                    $filePath = $groupFolder . DIRECTORY_SEPARATOR . $index . '_' . $filenameWithExtension;

                    //Check if the media was updated yesterday. If so we also need to refresh the image
                    $mediaUpdate = Carbon::createFromFormat('Y-m-d', $mediaArray['MediaUpdate']);
                    $now = Carbon::now();
                    $now = Carbon::create($now->year, $now->month, $now->day);
                    $needsUpdate = ($mediaUpdate && $now->subDay()->equalTo($mediaUpdate)) ? true : false;
                    if($needsUpdate) Log::debug('Going to download the following media item again since it was was updated yesterday: ' . $url);

                    if(! file_exists($filePath) || $needsUpdate) {
                        try {
                            $hSource = fopen($url, 'r');
                            $hDest = fopen($filePath, 'w');
                            while (! feof($hSource)) {
                                $chunk = fread($hSource, 5242880); //Read the url in a chunk of 5 MB
                                fwrite($hDest, $chunk); //Write the max 5MB of data to the file
                                unset($chunk); //Free up the 5MB of memory for garbage collection. Just to prevent memory exhaustion errors.
                            }
                            fclose($hSource);
                            fclose($hDest);
                        } catch(\Exception $exception) {
                            Log::debug('Tried to download a media file defined in the realworks xml but failed (' . $exception->getMessage() . '): ' . $url);
                            $this->mailOnError('Could not store media that needed to be download: ' . $exception->getMessage());
                            continue;
                        }
                    }
                }
            }

            return $object['MediaLijst'];
        }

        return [];
    }

    /**
     * @param array $object
     * @return bool
     */
    private function hasValidMainPhoto(array $object): bool
    {
        if(! isset($object['MediaLijst'])) return false;

        $mediaLijst = $object['MediaLijst'];
        if(! is_array($mediaLijst)) $mediaLijst = [$mediaLijst];

        foreach($mediaLijst as $mediaGroup) {
            if(! is_array($mediaGroup)) $mediaGroup = [$mediaGroup];

            foreach($mediaGroup as $mediaItem) {
                if(! is_array($mediaItem)) $mediaItem = ['Groep' => $mediaItem];

                if(! isset($mediaItem['Groep']) || $mediaItem['Groep'] !== 'HoofdFoto') continue;

                $url = $mediaItem['URL'] ?? null;
                if(! is_string($url) || trim($url) === '') return false;

                return true;
            }
        }

        return false;
    }

    private function getUrl()
    {
        $base = config('realworks.api_url');

        $queryParameters = [
            'koppeling' => 'WEBSITE',
            'user' => config('realworks.api_username'),
            'password' => config('realworks.api_password'),
            'og' => config('realworks.api_og_value'),
        ];

        if(config('realworks.api_documentation') == 'true') $queryParameters['documentatie'] = 'true';
        if(config('realworks.api_connected') == 'true') $queryParameters['connected'] = 'true';
        if(config('realworks.api_version')) $queryParameters['versie'] = config('realworks.api_version');
        if(config('realworks.api_office')) $queryParameters['kantoor'] = config('realworks.api_office');

        //String "true" or "false" from config is changed into a real true or false. Putting that in the http_build_query causes it to change to 0 or 1. While realworks expects "true" or "false" for boolean
        foreach($queryParameters as $index => $parameter)
        {
            if($parameter === true) $queryParameters[$index] = 'true';
            elseif($parameter === false) $queryParameters[$index] = 'false';
        }

        $url = $base . '?' . http_build_query($queryParameters);

        return $url;
    }

    /**
     * @param string $url
     * @return mixed
     */
    public static function getFilenameFromUrl(string $url)
    {
        $path = parse_url($url, PHP_URL_PATH);
        $exploded = explode('/', $path);

        return last($exploded);
    }

    /**
     * Creates a folder for the realworks zip and it's contents in laravels storage folder if needed and returns its absolute path
     *
     * @param string|null $subFolder
     * @return string
     */
    private static function createAndGetRealworksStorageFolder(string $subFolder = null):string
    {
        $folder = storage_path() . DIRECTORY_SEPARATOR . config('realworks.storagefolder_name', 'realworks') . DIRECTORY_SEPARATOR;
        self::createFolderIfNotExitsOrFail($folder);

        if($subFolder)
        {
            $absoluteSubFolder = self::createFolderIfNotExitsOrFail($folder . $subFolder);

            return $absoluteSubFolder;
        }

        return $folder;
    }

    /**
     * Creates a public folder for the frontend
     *
     * @param string|null $subFolder
     * @return string
     */
    private static function createAndGetPublicRealworksFolder(string $subFolder = null):string
    {
        $folder = public_path() . DIRECTORY_SEPARATOR . config('realworks.storagefolder_name', 'realworks') . DIRECTORY_SEPARATOR;
        self::createFolderIfNotExitsOrFail($folder);

        if($subFolder)
        {
            $absoluteSubFolder = $folder . $subFolder . DIRECTORY_SEPARATOR;
            self::createFolderIfNotExitsOrFail($absoluteSubFolder);

            return $absoluteSubFolder;
        }

        return $folder;
    }

    /**
     * @param $folder
     * @return mixed
     */
    private static function createFolderIfNotExitsOrFail($folder)
    {
        if(! file_exists($folder)) {
            if(! mkdir($folder, 0777, true)) throw new \RuntimeException('The realworks folder could not be created. ' . $folder);
        }

        return $folder;
    }

    /**
     * Recursively deletes a the contents in a directory and if you specify the boolean argument to true the directory itself too
     *
     * @param $dir
     * @param bool $deleteDir
     */
    private static function rrmdir($dir, $deleteDir = true) {
        if (is_dir($dir)) {
            $objects = scandir($dir);
            foreach ($objects as $object) {
                if ($object != '.' && $object != '..') {
                    if (filetype($dir . DIRECTORY_SEPARATOR . $object) == 'dir')
                        self::rrmdir($dir . DIRECTORY_SEPARATOR . $object);
                    else {
                        if(! unlink($dir . DIRECTORY_SEPARATOR . $object)) throw new \RuntimeException('Could not empty the temporary folder to store the exported results in. Please contact your website builder ' . $dir . '/' . $object);
                    }
                }
            }
            reset($objects);
            if($deleteDir) rmdir($dir);
        }
    }

    /**
     * @param string $message
     */
    private function mailOrShowErrorAndDie(string $message)
    {
        Log::error($message);

        $mail = (new RealworksFailMail($message));
        if(! \App::environment('local')) {
            \Mail::send($mail);
//            die();
        } else {
//            echo 'ERROR: '.$message.PHP_EOL;
//            die();

        }
    }

    /**
     * @param string $message
     */
    private function mailOnError(string $message)
    {
        Log::error($message);

        $mail = (new RealworksFailMail($message));
        if(! \App::environment('local')) {
            \Mail::send($mail);
        }

    }

    private function deletePublicFoldersFromNonExistingObjects()
    {
        //Get all realworks data property / object folders in the wwwroot
        $publicRealworksFolder = $this->createAndGetPublicRealworksFolder();
        $filesAndDirs = scandir($publicRealworksFolder);
        $dirs = array_filter($filesAndDirs, function ($item) use ($publicRealworksFolder) {
            $absolute = $publicRealworksFolder . $item;

            return is_dir($absolute) && $item != '.' && $item != '..';
        });

        //Get the system ids from the property / object folders in the wwwroot
        $objectSystemIdsFromStoredFiles = array_map(function ($dir) {
            return last(explode('-', $dir));
        }, $dirs);

        //Get all system ids from objects in the database
        $systemIdsPresentInDatabase = RealworkObject::get(['id', 'system_id'])->map(function (RealworkObject $object) {
            return $object->system_id;
        });

        //Create al list of system ids which have a folder in wwwroot property / object folder but don't have a database entry
        $systemIdsToRemoveFromPublicFolder = array_filter($objectSystemIdsFromStoredFiles, function ($systemId) use ($systemIdsPresentInDatabase) {
            if($systemIdsPresentInDatabase->contains($systemId)) {
                return false;
            } else {
                return true;
            }
        });

        //Delete the wwwroot property / object folders which don't have a databse entry
        foreach($systemIdsToRemoveFromPublicFolder as $systemIdToRemoveFromPublicFolder)
        {
            foreach($dirs as $dir)
            {
                $systemIdFromFolder = last(explode('-', $dir));
                if($systemIdToRemoveFromPublicFolder == $systemIdFromFolder) {
                    $folderToDelete = $absolute = $publicRealworksFolder . $dir;
//                    echo $folderToDelete.PHP_EOL;
                    self::rrmdir($folderToDelete, true);
                }
            }
        }
    }

    /**
     * Build an array of ids that are listed in the XML
     *
     * @param array $xml
     * @return array
     */
    private function extractObjectSystemIds(array $xml) {
        $xmlSystemIds = [];

        if(! array_key_exists('Object', $xml)) $xmlSystemIds; //Empty

        foreach($xml['Object'] as $objectArray)
        {
            if(! isset($objectArray['ObjectSystemID'])) $this->mailOrShowErrorAndDie('An object in the XML did not have a required ObjectSystemID. Stopped processing.');
            $xmlSystemIds[] = $objectArray['ObjectSystemID'];
        }

        return $xmlSystemIds;
    }

    /**
     * @return array
     */
    private function getObjectIdsMissingInSystemsIdArray(Collection $realworksObjects, array $ids) {
        //Loop over all our stored Object records and see if their are in the previously created xmlSystemIdsArray. If not, store them in an array.
        return $realworksObjects->map(function (RealworkObject $object) use ($ids) {
            if(! in_array((string) $object->system_id, $ids, true)) return $object->id;

            return null;
        })->filter(function ($systemId) { return $systemId !== null; })->toArray();
    }

    /**
     * Delete al Objects that match some conditions.
     *
     * @param array $xml
     */
    private function deleteObjectsFromDatabase(array $xml)
    {
        $this->createAndGetPublicRealworksFolder();

        $allObjects = RealworkObject::with('objectDetails.translations')->get();

        $xmlSystemIds = $this->extractObjectSystemIds($xml);
        $objectIdsMissingInXml = $this->getObjectIdsMissingInSystemsIdArray($allObjects, $xmlSystemIds);

        //Loop over all objects. Check if their ids exist in the objectIdsMissingInXml array. If so, return their ids based on some additional conditions.
        $objectIdsToDelete = $allObjects->filter(function (RealworkObject $realworksObject) use ($objectIdsMissingInXml) {
            //Check if this Object should be deleted sometime. If not return false to indicate that it should not.
            if(! in_array($realworksObject->id, $objectIdsMissingInXml)) return false;

            //Check if it has a dutch translation. If not it must be deleted directly.
           /** @var ObjectDetailsTranslation $dutchTranslation */
           $translation = $realworksObject->objectDetails->translations->where('language_id', '=', 104)->first();
           if(! $translation) return true;

           //If it is not "sold", delete it directly.
           if(strtolower($translation->status_beschikbaarheid) !== 'verkocht') return true;

           //If it is sold, we must keep it for a year
//           return Carbon::now()->subYear() >= $translation->updated_at;
           return Carbon::now()->subWeek(2) >= $translation->updated_at; //TODO. Comment this line when zelfverkopen has approved the change
        })->pluck('id')->toArray();

        RealworkObject::destroy($objectIdsToDelete);
    }

    private function updateYearMonthDate(Model $model, string $attribute, string $source) {
        $source = Carbon::createFromFormat('Y-m-d', $source);

        if(! $model->{$attribute}) $model->{$attribute} = Carbon::now();
        $carbon = is_string($model->{$attribute}) ? Carbon::parse($model->{$attribute}) : $model->{$attribute};

        if($carbon->year !== $source->year || $carbon->month !== $source->month || $carbon->day !== $source->day) {
            $carbon->year = $source->year;
            $carbon->month = $source->month;
            $carbon->day = $source->day;
            $model->{$attribute} = $carbon;
        }
    }
}