Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Allow importing the generated XML in a DOMDocument and improving HTML converter output #348

Merged
merged 1 commit into from
Jul 12, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,8 @@
},
"require": {
"php" : ">=7.0.10",
"ext-mbstring" : "*"
"ext-mbstring" : "*",
"ext-dom" : "*"
},
"require-dev": {
"ext-curl" : "*",
Expand Down
77 changes: 68 additions & 9 deletions src/HTMLConverter.php
Original file line number Diff line number Diff line change
Expand Up @@ -58,19 +58,45 @@ public function __construct()
/**
* Convert an Record collection into a DOMDocument.
*
* @param array|Traversable $records the tabular data collection
* @param array|Traversable $records The tabular data collection
* @param array $headerRecord An optional array of headers to output to the table using `<thead>` and `<th>` elements
* @param array $footerRecord An optional array of footers to output to the table using `<tfoot>` and `<th>` elements
*
* @return string
*/
public function convert($records): string
public function convert($records, array $headerRecord = [], array $footerRecord = []): string
{
/** @var DOMDocument $doc */
$doc = $this->xml_converter->convert($records);
if ([] === $headerRecord && [] === $footerRecord) {
/** @var DOMDocument $doc */
$doc = $this->xml_converter->convert($records);

/** @var DOMElement $table */
$table = $doc->getElementsByTagName('table')->item(0);
$this->styleTableElement($table);

return $doc->saveHTML($table);
};

$doc = new DOMDocument('1.0');

$tbody = $this->xml_converter->rootElement('tbody')->import($records, $doc);
$table = $doc->createElement('table');
$this->styleTableElement($table);
if (!empty($headerRecord)) {
$table->appendChild(
$this->createRecordRow('thead', 'th', $headerRecord, $doc)
);
}
$table->appendChild($tbody);
if (!empty($footerRecord)) {
$table->appendChild(
$this->createRecordRow('tfoot', 'th', $footerRecord, $doc)
);
}

/** @var DOMElement $table */
$table = $doc->getElementsByTagName('table')->item(0);
$table->setAttribute('class', $this->class_name);
$table->setAttribute('id', $this->id_value);
$doc->appendChild($table);

return $doc->saveHTML($table);
return $doc->saveHTML();
}

/**
Expand Down Expand Up @@ -111,4 +137,37 @@ public function td(string $fieldname_attribute_name): self

return $clone;
}

/**
* Create a DOMElement representing a single record of data
*
* @param string $recordTagName
* @param string $fieldTagName
* @param array $record
* @param DOMDocument $doc
*
* @return DOMElement
*/
private function createRecordRow(string $recordTagName, string $fieldTagName, array $record, DOMDocument $doc) : DOMElement
{
$node = $this->xml_converter->rootElement($recordTagName)->fieldElement($fieldTagName)->import([$record], $doc);

/** @var DOMElement $element */
foreach ($node->getElementsByTagName($fieldTagName) as $element) {
$element->setAttribute('scope', 'col');
}

return $node;
}

/**
* Style the table dom element
*
* @param DOMElement $table_element
*/
private function styleTableElement(DOMElement $table_element)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

return typed whenever you can

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Updated

{
$table_element->setAttribute('class', $this->class_name);
$table_element->setAttribute('id', $this->id_value);
}
}
29 changes: 25 additions & 4 deletions src/XMLConverter.php
Original file line number Diff line number Diff line change
Expand Up @@ -80,27 +80,48 @@ class XMLConverter
];

/**
* Convert an Record collection into a DOMDocument.
* Convert a Record collection into a DOMDocument.
*
* @param array|Traversable $records the CSV records collection
*
* @return DOMDocument
*/
public function convert($records): DOMDocument
{
if (!is_iterable($records)) {
throw new TypeError(sprintf('%s() expects argument passed to be iterable, %s given', __METHOD__, gettype($records)));
}
$doc = new DOMDocument('1.0');
$doc->appendChild(
$this->import($records, $doc)
);
return $doc;
}

/**
* Create a new DOMElement related to the given DOMDocument.
*
* **DOES NOT** attach to the DOMDocument
*
* @param iterable $records
*
* @return DOMElement
*/
public function import($records, DOMDocument $doc): DOMElement
{
if (!is_iterable($records)) {
nyamsprod marked this conversation as resolved.
Show resolved Hide resolved
throw new TypeError(sprintf('%s() expects argument passed to be iterable, %s given', __METHOD__, gettype($records)));
}

$field_encoder = $this->encoder['field']['' !== $this->column_attr];
$record_encoder = $this->encoder['record']['' !== $this->offset_attr];
$doc = new DOMDocument('1.0');
$root = $doc->createElement($this->root_name);
foreach ($records as $offset => $record) {
$node = $this->$record_encoder($doc, $record, $field_encoder, $offset);
$root->appendChild($node);
}
$doc->appendChild($root);

return $doc;
return $root;
}

/**
Expand Down
53 changes: 53 additions & 0 deletions tests/HTMLConverterTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,59 @@ public function testToHTML()
self::assertContains('<table class="table-csv-data" id="test">', $html);
self::assertContains('<tr data-record-offset="', $html);
self::assertContains('<td title="', $html);
self::assertNotContains('<thead>', $html);
self::assertNotContains('<tbody>', $html);
self::assertNotContains('<tfoot>', $html);
}

/**
* @covers ::__construct
* @covers ::table
* @covers ::tr
* @covers ::td
* @covers ::convert
*/
public function testToHTMLWithHeaders()
{
$csv = Reader::createFromPath(__DIR__.'/data/prenoms.csv', 'r')
->setDelimiter(';')
->setHeaderOffset(0)
;

$stmt = (new Statement())
->offset(3)
->limit(5)
;

$records = $stmt->process($csv);

$converter = (new HTMLConverter())
->table('table-csv-data', 'test')
->td('title')
->tr('data-record-offset')
;

$html = $converter->convert($records, $records->getHeader());
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

if you only provide a thead why does the tfoot is present 🤔

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Honestly, I assumed that they came as a threesome...
I originally hid them accordingly, would you you prefer I did that again?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yes hides them accordingly ... I prefer this to be explicit. I was unable to find a resource stating for instance that if you use tfoot then thead is required. The only requirement is that if one is present then tbody must be used.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done

self::assertContains('<table class="table-csv-data" id="test">', $html);
self::assertContains('<th scope="col">prenoms</th>', $html);
self::assertContains('<thead>', $html);
self::assertContains('<tbody>', $html);
self::assertNotContains('<tfoot>', $html);

$html = $converter->convert($records, [], $records->getHeader());
self::assertContains('<table class="table-csv-data" id="test">', $html);
self::assertContains('<th scope="col">prenoms</th>', $html);
self::assertNotContains('<thead>', $html);
self::assertContains('<tbody>', $html);
self::assertContains('<tfoot>', $html);

$html = $converter->convert($records, $records->getHeader(), $records->getHeader());
self::assertContains('<table class="table-csv-data" id="test">', $html);
self::assertContains('<thead>', $html);
self::assertContains('<tbody>', $html);
self::assertContains('<tfoot>', $html);
self::assertNotContains('<thead><tr data-record-offset="0"></tr></thead>', $html);
self::assertNotContains('<tfoot><tr data-record-offset="0"></tr></tfoot>', $html);
}

/**
Expand Down
42 changes: 42 additions & 0 deletions tests/XMLConverterTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
namespace LeagueTest\Csv;

use DOMDocument;
use DOMElement;
use DOMException;
use League\Csv\Reader;
use League\Csv\Statement;
Expand Down Expand Up @@ -90,4 +91,45 @@ public function testXmlElementTriggersTypeError()
self::expectException(TypeError::class);
(new XMLConverter())->convert('foo');
}

/**
* @covers ::rootElement
* @covers ::recordElement
* @covers ::fieldElement
* @covers ::import
* @covers ::recordToElement
* @covers ::recordToElementWithAttribute
* @covers ::fieldToElement
* @covers ::fieldToElementWithAttribute
* @covers ::filterAttributeName
* @covers ::filterElementName
*/
public function testImport()
{
$csv = Reader::createFromPath(__DIR__.'/data/prenoms.csv', 'r')
->setDelimiter(';')
->setHeaderOffset(0)
;

$stmt = (new Statement())
->offset(3)
->limit(5)
;

$records = $stmt->process($csv);

$converter = (new XMLConverter())
->rootElement('csv')
->recordElement('record', 'offset')
->fieldElement('field', 'name')
;

$doc = new DOMDocument('1.0');
$element = $converter->import($records, $doc);

self::assertInstanceOf(DOMDocument::class, $doc);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍

self::assertCount(0, $doc->childNodes);
self::assertInstanceOf(DOMElement::class, $element);
self::assertCount(5, $element->childNodes);
}
}