diff --git a/example/test-memo.php b/example/test-memo.php
index 228ba3e..7dc6df1 100644
--- a/example/test-memo.php
+++ b/example/test-memo.php
@@ -20,7 +20,7 @@
die('Bad path to file realpath '.$argv[1]);
}
-$table = new Table($filepath, null, 'cp1252');
+$table = new Table($filepath, ['encoding' => 'cp1252']);
echo 'Record count: '.$table->getRecordCount() . PHP_EOL;
$columns = $table->getColumns();
diff --git a/src/XBase/BlocksMerger.php b/src/XBase/BlocksMerger.php
index bcc6c9f..9f2eeb2 100644
--- a/src/XBase/BlocksMerger.php
+++ b/src/XBase/BlocksMerger.php
@@ -30,6 +30,9 @@ public function get(): array
return $this->squeeze();
}
+ /**
+ * Combines several adjacent blocks into one.
+ */
private function squeeze(): array
{
$pointers = array_column($this->blocksToDelete, 0);
diff --git a/src/XBase/Memo/AbstractMemo.php b/src/XBase/Memo/AbstractMemo.php
index bd81586..8ad24bd 100644
--- a/src/XBase/Memo/AbstractMemo.php
+++ b/src/XBase/Memo/AbstractMemo.php
@@ -16,23 +16,38 @@ abstract class AbstractMemo implements MemoInterface
/** @var string */
protected $filepath;
- /** @var string */
- protected $convertFrom;
+ /** @var array */
+ protected $options = [];
/**
- * Memo constructor.
- *
- * @param string $convertFrom
+ * @param string $filepath Path to memo file
+ * @param array $options Array of options:
+ * encoding - convert text data from
+ * writable - edit mode
*/
- public function __construct(Table $table, string $filepath, ?string $convertFrom = null)
+ public function __construct(Table $table, string $filepath, $options = [])
{
$this->table = $table;
$this->filepath = $filepath;
- $this->convertFrom = $convertFrom; //todo autodetect from languageCode
+ $this->options = $this->resolveOptions($options);
$this->open();
$this->readHeader();
}
+ protected function resolveOptions($options = []): array
+ {
+ if (is_string($options)) {
+ @trigger_error('You should pass convertFrom as `encoding` option');
+ $options = ['encoding' => $options];
+ }
+
+ $options = array_merge([
+ 'encoding' => null,
+ ], $options);
+
+ return $options;
+ }
+
public function __destruct()
{
$this->close();
diff --git a/src/XBase/Memo/AbstractWritableMemo.php b/src/XBase/Memo/AbstractWritableMemo.php
index 5bc657a..0a257fa 100644
--- a/src/XBase/Memo/AbstractWritableMemo.php
+++ b/src/XBase/Memo/AbstractWritableMemo.php
@@ -4,16 +4,12 @@
use XBase\BlocksMerger;
use XBase\Stream\Stream;
-use XBase\Table;
use XBase\Traits\CloneTrait;
abstract class AbstractWritableMemo extends AbstractMemo implements WritableMemoInterface
{
use CloneTrait;
- /** @var bool */
- protected $writable = false;
-
/**
* @var BlocksMerger Garbage blocks. Delete blocks while saving.
*/
@@ -26,15 +22,16 @@ abstract protected function getBlockLengthInBytes(): int;
abstract protected function calculateBlockCount(string $data): int;
- public function __construct(Table $table, string $filepath, ?string $convertFrom = null, bool $writable = false)
+ protected function resolveOptions($options = []): array
{
- $this->writable = $writable;
- parent::__construct($table, $filepath, $convertFrom);
+ return array_merge([
+ 'writable' => false,
+ ], parent::resolveOptions($options));
}
public function open(): void
{
- if (!$this->writable) {
+ if (!$this->options['writable']) {
parent::open();
return;
@@ -54,7 +51,7 @@ protected function readHeader(): void
public function close(): void
{
parent::close();
- if ($this->writable && $this->cloneFilepath) {
+ if ($this->options['writable'] && $this->cloneFilepath) {
unlink($this->cloneFilepath);
$this->cloneFilepath = null;
}
diff --git a/src/XBase/Memo/DBase3Memo.php b/src/XBase/Memo/DBase3Memo.php
index 0073381..a51fe76 100644
--- a/src/XBase/Memo/DBase3Memo.php
+++ b/src/XBase/Memo/DBase3Memo.php
@@ -47,8 +47,8 @@ public function get(int $pointer): ?MemoObject
if (chr(0x00) === substr($result, -1)) {
$result = substr($result, 0, -1); // remove endline symbol (0x00)
}
- if ($this->convertFrom) {
- $result = iconv($this->convertFrom, 'utf-8', $result);
+ if ($this->options['encoding']) {
+ $result = iconv($this->options['encoding'], 'utf-8', $result);
}
}
diff --git a/src/XBase/Memo/DBase4Memo.php b/src/XBase/Memo/DBase4Memo.php
index d79d1d0..c535469 100644
--- a/src/XBase/Memo/DBase4Memo.php
+++ b/src/XBase/Memo/DBase4Memo.php
@@ -46,8 +46,8 @@ public function get(int $pointer): ?MemoObject
// $result = $this->fp->read($memoLength[1] - self::BLOCK_SIGN_LENGTH - self::BLOCK_LENGTH_LENGTH);
$type = $this->guessDataType($result);
- if (MemoObject::TYPE_TEXT === $type && $this->convertFrom) {
- $result = iconv($this->convertFrom, 'utf-8', $result);
+ if (MemoObject::TYPE_TEXT === $type && $this->options['encoding']) {
+ $result = iconv($this->options['encoding'], 'utf-8', $result);
}
return new MemoObject($result, $type, $pointer, $memoLength[1]);
diff --git a/src/XBase/Memo/FoxproMemo.php b/src/XBase/Memo/FoxproMemo.php
index 7c86683..d974424 100644
--- a/src/XBase/Memo/FoxproMemo.php
+++ b/src/XBase/Memo/FoxproMemo.php
@@ -54,8 +54,8 @@ public function get(int $pointer): ?MemoObject
$result = $this->fp->read($memoLength[1]);
$type = $this->guessDataType($result);
- if ($this->convertFrom) {
- $result = iconv($this->convertFrom, 'utf-8', $result);
+ if ($this->options['encoding']) {
+ $result = iconv($this->options['encoding'], 'utf-8', $result);
}
return new MemoObject($result, $type, $pointer, $memoLength[1]);
diff --git a/src/XBase/Memo/MemoFactory.php b/src/XBase/Memo/MemoFactory.php
index c1f27e6..7038b1b 100644
--- a/src/XBase/Memo/MemoFactory.php
+++ b/src/XBase/Memo/MemoFactory.php
@@ -7,7 +7,7 @@
class MemoFactory
{
- public static function create(Table $table, bool $writable = false): ?MemoInterface
+ public static function create(Table $table, array $options = []): ?MemoInterface
{
$class = self::getClass($table->getVersion());
$refClass = new \ReflectionClass($class);
@@ -26,7 +26,7 @@ public static function create(Table $table, bool $writable = false): ?MemoInterf
return null; //todo create file?
}
- return $refClass->newInstance($table, $memoFilepath, $table->getConvertFrom(), $writable);
+ return $refClass->newInstance($table, $memoFilepath, $options);
}
private static function getClass(int $version): string
diff --git a/src/XBase/Table.php b/src/XBase/Table.php
index 88d7873..18f7af9 100755
--- a/src/XBase/Table.php
+++ b/src/XBase/Table.php
@@ -30,9 +30,6 @@ class Table
/** @var string Table filepath. */
protected $filepath;
- /** @var array|null */
- protected $availableColumns;
-
/** @var Stream */
protected $fp;
@@ -48,9 +45,6 @@ class Table
/** @var RecordInterface|null */
protected $record;
- /** @var string|null */
- protected $convertFrom;
-
/**
* @var int
*
@@ -137,25 +131,51 @@ class Table
/** @var string|null DBase7 only */
protected $languageName;
+ /** @var array */
+ protected $options = [];
+
/**
* Table constructor.
*
- * @param array|null $availableColumns
- * @param string|null $convertFrom Encoding of file
+ * @param array $options Array of options:
+ * encoding - convert text data from
+ * columns - available columns
*
* @throws \Exception
*/
- public function __construct(string $filepath, $availableColumns = null, $convertFrom = null)
+ public function __construct(string $filepath, $options = [], $convertFrom = null)
{
$this->filepath = $filepath;
- $this->availableColumns = $availableColumns;
- $this->convertFrom = $convertFrom; //todo autodetect from languageCode
+ $this->options = $this->resolveOptions($options, $convertFrom);
$this->open();
$this->readHeader();
$this->openMemo();
}
+ protected function resolveOptions($options, $convertFrom = null): array
+ {
+ // right options
+ if (!empty($options) && array_intersect(['encoding', 'columns'], array_keys($options))) {
+ return array_merge([
+ 'columns' => [],
+ 'encoding' => null,
+ ], $options);
+ }
+
+ if (!empty($options)) {
+ @trigger_error('You should pass availableColumns as `columns` option');
+ }
+ if (!empty($convertFrom)) {
+ @trigger_error('You should pass convertFrom as `encoding` option');
+ }
+
+ return [
+ 'columns' => $options ?? [],
+ 'encoding' => $convertFrom,
+ ];
+ }
+
protected function open(): void
{
if (!file_exists($this->filepath)) {
@@ -206,7 +226,7 @@ protected function readHeader(): void
protected function openMemo(): void
{
if (TableType::hasMemo($this->getVersion())) {
- $this->memo = MemoFactory::create($this);
+ $this->memo = MemoFactory::create($this, $this->options);
}
}
@@ -517,7 +537,7 @@ public function getDeleteCount()
public function getConvertFrom(): ?string
{
- return $this->convertFrom;
+ return $this->options['encoding'];
}
/**
diff --git a/src/XBase/Traits/CloneTrait.php b/src/XBase/Traits/CloneTrait.php
index ed833d1..da92580 100644
--- a/src/XBase/Traits/CloneTrait.php
+++ b/src/XBase/Traits/CloneTrait.php
@@ -7,7 +7,7 @@
*/
trait CloneTrait
{
- /** @var string */
+ /** @var string|null */
private $cloneFilepath;
/**
diff --git a/src/XBase/WritableTable.php b/src/XBase/WritableTable.php
index cb86d5e..e0d0b0d 100644
--- a/src/XBase/WritableTable.php
+++ b/src/XBase/WritableTable.php
@@ -18,6 +18,16 @@ class WritableTable extends Table
{
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
*
@@ -30,29 +40,48 @@ class WritableTable extends Table
*/
private $insertion = false;
+ protected function resolveOptions($options, $convertFrom = null): array
+ {
+ return array_merge(
+ ['editMode' => self::EDIT_MODE_CLONE],
+ parent::resolveOptions($options, $convertFrom)
+ );
+ }
+
protected function open(): void
{
- $this->clone();
- $this->fp = Stream::createFromFile($this->cloneFilepath, 'rb+');
+ switch ($this->options['editMode']) {
+ case self::EDIT_MODE_CLONE:
+ $this->clone();
+ $this->fp = Stream::createFromFile($this->cloneFilepath, 'rb+');
+ break;
+
+ case self::EDIT_MODE_REALTIME:
+ $this->fp = Stream::createFromFile($this->filepath, 'rb+');
+ break;
+ }
}
protected function openMemo(): void
{
if (TableType::hasMemo($this->getVersion())) {
- $this->memo = MemoFactory::create($this, true);
+ $memoOptions = array_merge($this->options, ['writable' => true]);
+ $this->memo = MemoFactory::create($this, $memoOptions);
}
}
public function close(): void
{
- if ($this->autoSave) {
+ if (self::EDIT_MODE_CLONE === $this->options['editMode'] && $this->autoSave) {
@trigger_error('You should call `save` method directly.');
$this->save();
}
parent::close();
- unlink($this->cloneFilepath);
+ if ($this->cloneFilepath && file_exists($this->cloneFilepath)) {
+ unlink($this->cloneFilepath);
+ }
}
/**
@@ -185,29 +214,6 @@ protected function writeHeader(): void
foreach ($this->columns as $column) {
$column->toBinaryString($this->fp);
-// $this->fp->write(str_pad(substr($column->getRawName(), 0, 11), 11, chr(0))); // 0-10
-// $this->fp->write($column->getType());// 11
-// $this->fp->writeUInt($column->getMemAddress());//12-15
-// $this->fp->writeUChar($column->getLength());//16
-// $this->fp->writeUChar($column->getDecimalCount());//17
-// $this->fp->write(
-// method_exists($column, 'getReserved1')
-// ? call_user_func([$column, 'getReserved1'])
-// : str_pad('', 2, chr(0))
-// );//18-19
-// $this->fp->writeUChar($column->getWorkAreaID());//20
-// $this->fp->write(
-// method_exists($column, 'getReserved2')
-// ? call_user_func([$column, 'getReserved2'])
-// : str_pad('', 2, chr(0))
-// );//21-22
-// $this->fp->write(chr($column->isSetFields() ? 1 : 0));//23
-// $this->fp->write(
-// method_exists($column, 'getReserved3')
-// ? call_user_func([$column, 'getReserved3'])
-// : str_pad('', 7, chr(0))
-// );//24-30
-// $this->fp->write(chr($column->isIndexed() ? 1 : 0));//31
}
$this->fp->writeUChar(0x0d);
@@ -247,6 +253,10 @@ public function writeRecord(RecordInterface $record = null): self
$this->fp->flush();
+ if (self::EDIT_MODE_REALTIME === $this->options['editMode'] && $this->insertion) {
+ $this->save();
+ }
+
$this->insertion = false;
return $this;
@@ -305,11 +315,14 @@ public function pack(): self
}
$this->recordCount = $newRecordCount;
- $this->writeHeader();
$size = $this->headerLength + ($this->recordCount * $this->recordByteLength);
$this->fp->truncate($size);
+ if (self::EDIT_MODE_REALTIME === $this->options['editMode']) {
+ $this->save();
+ }
+
return $this;
}
@@ -327,7 +340,9 @@ public function save(): self
$this->fp->writeUChar(self::END_OF_FILE_MARKER);
}
- copy($this->cloneFilepath, $this->filepath);
+ if (self::EDIT_MODE_CLONE === $this->options['editMode']) {
+ copy($this->cloneFilepath, $this->filepath);
+ }
return $this;
}
@@ -335,7 +350,7 @@ public function save(): self
/**
* @internal
*
- * @todo Find better solution for notifying table from memo.
+ * @todo Find better solution to notify table from Memo.
*/
public function onMemoBlocksDelete(array $blocks): void
{
diff --git a/tests/Writable/DBase3TableTest.php b/tests/Writable/DBase3TableTest.php
index 28d605d..849f844 100644
--- a/tests/Writable/DBase3TableTest.php
+++ b/tests/Writable/DBase3TableTest.php
@@ -15,7 +15,7 @@ class DBase3TableTest extends TestCase
public function testAppendRecord(): void
{
- self:self::markTestIncomplete();
+ self::markTestIncomplete();
$copyTo = $this->duplicateFile(self::FILEPATH);
$size = filesize($copyTo);
diff --git a/tests/Writable/FoxproTableTest.php b/tests/Writable/FoxproTableTest.php
index e1476cd..8d8b3ba 100644
--- a/tests/Writable/FoxproTableTest.php
+++ b/tests/Writable/FoxproTableTest.php
@@ -14,7 +14,7 @@ class FoxproTableTest extends TestCase
const FILEPATH = __DIR__.'/../Resources/foxpro/Foxpro2.dbf';
/**
- * Method appendRecord must not increase recordCount. Only after writeRecord will it will be increased.
+ * Method appendRecord must not increase recordCount. Only after call writeRecord it will be increased.
*/
public function testAppendNotIncreaseRecordsCount(): void
{
diff --git a/tests/WritableTableTest.php b/tests/WritableTableTest.php
index d0e2eb0..fcd9e5f 100644
--- a/tests/WritableTableTest.php
+++ b/tests/WritableTableTest.php
@@ -179,4 +179,78 @@ public function testIssue78(): void
self::assertSame(5000.0, $record->getNum('salario'));
$table->close();
}
+
+ /**
+ * Issue #97
+ * Write data in clone mode. Need to call `save` method.
+ */
+ public function testCloneMode(): void
+ {
+ $copyTo = $this->duplicateFile(self::FILEPATH);
+
+ $tableRead = new Table($copyTo, ['encoding' => 'cp866']);
+ $recordsCountBeforeInsert = $tableRead->getRecordCount();
+ $tableRead->close();
+
+ $tableWrite = new WritableTable($copyTo, [
+ 'encoding' => 'cp866',
+ 'editMode' => 'clone',
+ ]);
+ $recordWrite = $tableWrite->appendRecord();
+ $recordWrite
+ ->set('regn', 2)
+ ->set('plan', 'Ы');
+ $tableWrite->writeRecord($recordWrite);
+
+ // nothing has changed
+ $tableRead = new Table($copyTo, ['encoding' => 'cp866']);
+ self::assertSame($recordsCountBeforeInsert, $tableRead->getRecordCount());
+ $tableRead->close();
+
+ // save changes
+ $tableWrite
+ ->save()
+ ->close();
+
+ $tableRead = new Table($copyTo, ['encoding' => 'cp866']);
+ self::assertSame($recordsCountBeforeInsert + 1, $tableRead->getRecordCount());
+ $recordRead = $tableRead->pickRecord($recordsCountBeforeInsert);
+ self::assertNotEmpty($recordRead);
+ self::assertSame(2, $recordRead->get('regn'));
+ self::assertSame('Ы', $recordRead->get('plan'));
+ $tableRead->close();
+ }
+
+ /**
+ * Issue #97
+ * Write data in realtime mode. No need to call `save` method.
+ */
+ public function testRealtimeMode(): void
+ {
+ $copyTo = $this->duplicateFile(self::FILEPATH);
+
+ $tableRead = new Table($copyTo, ['encoding' => 'cp866']);
+ $recordsCountBeforeInsert = $tableRead->getRecordCount();
+ $tableRead->close();
+
+ $tableWrite = new WritableTable($copyTo, [
+ 'encoding' => 'cp866',
+ 'editMode' => 'realtime',
+ ]);
+ $recordWrite = $tableWrite->appendRecord();
+ $recordWrite
+ ->set('regn', 2)
+ ->set('plan', 'Ы');
+ $tableWrite->writeRecord($recordWrite);
+
+ $tableRead = new Table($copyTo, ['encoding' => 'cp866']);
+ self::assertSame($recordsCountBeforeInsert + 1, $tableRead->getRecordCount());
+ $recordRead = $tableRead->pickRecord($recordsCountBeforeInsert);
+ self::assertNotEmpty($recordRead);
+ self::assertSame(2, $recordRead->get('regn'));
+ self::assertSame('Ы', $recordRead->get('plan'));
+
+ $tableWrite->close();
+ $tableRead->close();
+ }
}