hisamu/php-xbase

View on GitHub
src/TableEditor.php

Summary

Maintainability
B
5 hrs
Test Coverage
A
97%
<?php declare(strict_types=1);

namespace XBase;

use XBase\Column\ColumnInterface;
use XBase\Enum\TableType;
use XBase\Record\RecordFactory;
use XBase\Record\RecordInterface;
use XBase\Stream\Stream;
use XBase\Table\Saver as TableSaver;
use XBase\Traits\CloneTrait;

class TableEditor extends TableReader
{
    use CloneTrait;

    /**
     * Perform any edits on clone file and replace original file after call `save` method.
     */
    public const EDIT_MODE_CLONE = 'clone';

    /**
     * Perform edits immediately on original file.
     */
    public const EDIT_MODE_REALTIME = 'realtime';

    /**
     * @var bool record property is new
     */
    private $insertion = false;

    protected function resolveOptions(array $options): array
    {
        $options = array_merge(['editMode' => self::EDIT_MODE_CLONE], $options);

        return array_merge(
            parent::resolveOptions($options),
            $options
        );
    }

    protected function open(): void
    {
        switch ($this->table->options['editMode']) {
            case self::EDIT_MODE_CLONE:
                $this->clone();
                $this->table->stream = Stream::createFromFile($this->cloneFilepath, 'rb+');
                break;

            case self::EDIT_MODE_REALTIME:
                $this->table->stream = Stream::createFromFile($this->getFilepath(), 'rb+');
                break;
        }

        //todo find better place for this
        $this->table->handlers['onMemoBlocksDelete'] = function (array $blocks): void {
            $columns = $this->getMemoColumns();

            for ($i = 0; $i < $this->getHeader()->recordCount; $i++) {
                $record = $this->pickRecord($i);
                $save = false;
                foreach ($columns as $column) {
                    if (!$pointer = $record->getGenuine($column->getName())) {
                        continue;
                    }

                    $sub = 0;
                    foreach ($blocks as $deletedPointer => $length) {
                        if ($pointer && $pointer > $deletedPointer) {
                            $sub += $length;
                        }
                    }
                    $save = $sub > 0;
                    $record->setGenuine($column->getName(), $pointer - $sub);
                }
                if ($save) {
                    $this->writeRecord($record);
                }
            }
        };
    }

    public function close(): void
    {
        parent::close();

        if ($this->cloneFilepath && file_exists($this->cloneFilepath)) {
            unlink($this->cloneFilepath);
        }
    }

    public function appendRecord(): RecordInterface
    {
        $this->recordPos = $this->getHeader()->recordCount;
        $this->record = RecordFactory::create($this->table, $this->encoder, $this->recordPos);
        $this->insertion = true;

        return $this->record;
    }

    public function writeRecord(RecordInterface $record = null): self
    {
        $record = $record ?? $this->record;
        if (!$record) {
            return $this;
        }

        $offset = $this->getHeader()->length + ($record->getRecordIndex() * $this->getHeader()->recordByteLength);
        $this->getStream()->seek($offset);
        $this->getStream()
            ->write(RecordFactory::createDataConverter($this->table, $this->encoder)
            ->toBinaryString($record));

        if ($this->insertion) {
            $this->table->header->recordCount++;
        }

        $this->getStream()->flush();

        if (self::EDIT_MODE_REALTIME === $this->table->options['editMode'] && $this->insertion) {
            $this->save();
        }

        $this->insertion = false;

        return $this;
    }

    public function deleteRecord(?RecordInterface $record = null): self
    {
        if ($this->record && $this->insertion) {
            $this->record = null;
            $this->recordPos = -1;

            return $this;
        }

        $record = $record ?? $this->record;
        if (!$record) {
            return $this;
        }

        $record->setDeleted(true);
        $this->writeRecord($record);

        return $this;
    }

    public function undeleteRecord(?RecordInterface $record = null): self
    {
        $record = $record ?? $this->record;
        if (!$record || false === $record->isDeleted()) {
            return $this;
        }

        $record->setDeleted(false);

        $this->getStream()->seek($this->getHeader()->length + ($record->getRecordIndex() * $this->getHeader()->recordByteLength));
        $this->getStream()->write(' ');
        $this->getStream()->flush();

        return $this;
    }

    /**
     * Remove deleted records.
     */
    public function pack(): self
    {
        $newRecordCount = 0;
        for ($i = 0; $i < $this->getRecordCount(); $i++) {
            $r = $this->moveTo($i);

            if ($r->isDeleted()) {
                // remove memo columns
                foreach ($this->getMemoColumns() as $column) {
                    if ($pointer = $this->record->getGenuine($column->getName())) {
                        $this->getMemo()->delete($pointer);
                    }
                }
                continue;
            }

            $r->setRecordIndex($newRecordCount++);
            $this->writeRecord($r);
        }

        $this->getHeader()->recordCount = $newRecordCount;

        $size = $this->getHeader()->length + ($newRecordCount * $this->getHeader()->recordByteLength);
        $this->getStream()->truncate($size);

        if (self::EDIT_MODE_REALTIME === $this->table->options['editMode']) {
            $this->save();
        }

        return $this;
    }

    public function save(): self
    {
        if ($memo = $this->getMemo()) {
            $memo->save();
        }

        $saver = new TableSaver($this->table);
        $saver->save();

        if (self::EDIT_MODE_CLONE === $this->table->options['editMode']) {
            copy($this->cloneFilepath, $this->getFilepath());
        }

        return $this;
    }

    /**
     * @return ColumnInterface[]
     */
    private function getMemoColumns(): array
    {
        $result = [];
        foreach ($this->getColumns() as $column) {
            if (in_array($column->getType(), TableType::getMemoTypes($this->getHeader()->version))) {
                $result[] = $column;
            }
        }

        return $result;
    }
}