I use Parsedown for transforming my Markdown into HTML like:
$pd->text('# My First heading');
In want automatically use the first site heading (h1) as an entry for a custom menu. I already had a look into the source, but couldn't figure out a good way to get all headings.
Perhaps there is something in elements function. Does anyone has an idea to get all or at least the first heading.
#tobias-redmann
couldn't figure out a good way to get all headings
I assume you already solved this problem. But since I had the same problem and came up with the answer, I would like to share some info for those who will confront with the same problem.
TL;DR
Getting ToC (Table of Contents) w/ Parsedown.
<?php
include('Parsedown.php');
class Extension extends Parsedown
{
public $array_line = array();
// Override
protected function blockHeader($Line)
{
// Parse $Line to parent class
$Block = Parsedown::blockHeader($Line);
// Set headings
if(isset($Block['element']['name'])){
$Level = (integer) trim($Block['element']['name'],'h');
$this->array_line[] = [
'text' => $Block['element']['name'],
'level' => $Level,
];
}
return $Block;
}
}
$text = file_get_contents('YourMarkdownFile.md');
$Parsedown = new Extension();
$string_body = $Parsedown->text($text);
$array_ToC = $Parsedown->array_line;
print_r($array_ToC);
//echo $string_body;
TS;DR (Details)
First of all, as #emanuil-rusev said, overriding the blockHeader method, you can get all the headings from the markdown text.
More specifically, when you call text($text) method, it parses the markdown strings given.
While parsing, the blockHeader($Line) method will be called every line with an array argument such as below.
$Line => [
"body" => "# SampleHead",
"indent" => 0,
"text" => "# SampleHead",
]
The original (parent) class' blockHeader($Line) method converts $Line into the following array($Block).
$Block => [
"element" => [
"name" => "h1",
"text" => "# SampleHead",
"handler" => "line",
],
]
So, the easiest way to get all the headings is to store these each time into an array.
Full script (extension) to get table of contents
See my repo:
https://github.com/KEINOS/parsedown-extension_table-of-contents
Author of Parsedown here.
You should be able to achieve this by creating an extension that overrides the blockHeader method. Basically, the overriding method should modify the result of the parent method.
Have a look at the Parsedown wiki for more info.
Related
This will be my very first question on here. I hope I give you all the info you need.
I am running a personal project using silverstripe v4.8
In my template, I have a optionsetfield, which is basically just a radiofield.
The first 3 items in the options, I have hardcoded.
I wrote another function for the rest to basically get me all the names of people who can make events, loop through them, and add them as options.
When I dump my outcome, it seems to come out the way I want it:
array (size=1)
'Rick' => string 'Rick' (length=4)
But when I try to see it in my template, it gives me:
Object->__call(): the method 'forTemplate' does not exist on 'SilverStripe\View\ArrayData'
Now when I don't add the function to my Optionset, the first 3 hardcoded items work fine.
I will post my OptionsetField and the other function below.
Thank you in advance
public function createEventsFilterForm()
{
$form = Form::create();
$form = FieldList::create([
OptionsetField::create('Eventfilters')
->setTitle('')
->setSource([
'past' => 'Verlopen',
'today' => 'Vandaag',
'future' => 'Toekomst',
$this->getFirstNameOfEvents()
])
]);
return $form;
}
public function getFirstNameOfEvents()
{
$allEvents = UpcomingEvents::get();
foreach ($allEvents as $event) {
$firstName = 'NVT';
$memberProfileID = $event->MemberProfileID;
if ($memberProfileID) {
$firstName = [MemberProfile::get()->byID($memberProfileID)->FirstName];
}
$output = ArrayLib::valuekey($firstName);
return $output;
}
}
tl;dr:
SilverStripe templates cannot handle arrays. I guess the array got automatically converted to an ArrayData object.
if you want to be more explicit, you can write:
return new ArrayData([
'Name' => "FOOBAR"
]);
and then in the template:
$FirstNameOfEvent <!-- this will also cause this error, because SSViwer does not know how to render ArrayData -->
<!-- but you can access object properties, like the Name that we defined: -->
$FirstNameOfEvent.Name <!-- will output FOOBAR -->
Long explanation:
forTemplate is a called by the SSViewer when rendering objects.
Basically, it's the SilverStripe equivalent to __toString(), whenever you are trying to output a object to the browser in a SilverStripe template, SSViewer (the renderer) will call forTemplate on that object.
Let me give an example:
class Foo extends VieableData {
public $message = 'Hello World';
public function forTemplate() {
return "This is a foo object with the message '{$this->message}'";
}
}
class PageController extends ContentController {
public function MyFooObject() {
return new Foo();
}
}
so if in your Page.ss template, you call $MyFooObject it will call the function of the same name and get an object. Because it's an object, SSViewer doesn't know how to render and will call Foo->forTemplate(). Which then will result in the output This is a foo object with the message 'Hello World'
ArrayData does not have a forTemplate method, thus you get the error. There are 2 ways to get around that[1]:
subclass ArrayData and implement a forTemplate method that turns your data into a string (or DBField object) that can be output to the browser
Don't try to render ArrayData in your Template, instead access the data directly (like in the tl;dr above, so $MyArrayData.MyField)[2]
[1]: the same is true for all objects
[2]: accessing object properties directly is always possible, even if you have a forTemplate method. forTemplate is just the default what to do if you don't specify a property.
EDIT:
sorry, I partially misunderstood your question/problem.
All the stuff I said above is still true, and important to understand, but it didn't answer your question.
I thought you are calling $getFirstNameOfEvents in the template, but actually, you are using it in a DropDownField (missed that part).
The thing about the SilverStripe CMS is, it also use the same templates system as the frontend for it's own things. So DropDownField will also use SSViewer to render. So my explanation is still true, it just happens inside DropDownField.ss which is a builtin template file. It does something like this:
<select>
<% loop $Source %>
<option value="$Key">$Value</option>
<% end_loop %>
</select>
$Source here is your array ['past' => 'Verlopen', 'today' => 'Vandaag', 'future' => 'Toekomst', $this->getFirstNameOfEvents()] which is automatically converted into ArrayData objects.
Now, the problem is, it doesn't work the way you think it works:
// the result you want:
['past' => 'Verlopen', 'today' => 'Vandaag', 'future' => 'Toekomst', 'Rick' => 'Rick']
// the result your code produces:
['past' => 'Verlopen', 'today' => 'Vandaag', 'future' => 'Toekomst', 0 => ['Rick' => 'Rick']]
notice how you have an array inside an array. Because getFirstNameOfEvents returns an array.
So what you should actually do:
$source = ['past' => 'Verlopen', 'today' => 'Vandaag', 'future' => 'Toekomst'];
$source = array_merge($source, $this->getFirstNameOfEvents());
$form = FieldList::create([
OptionsetField::create('Eventfilters')
->setTitle('')
->setSource($source)
]);
Given a variable that holds this string:
$property = 'parent->requestdata->inputs->firstname';
And an object:
$obj->parent->requestdata->inputs->firstname = 'Travis';
How do I access the value 'Travis' using the string? I tried this:
$obj->{$property}
But it looks for a property called 'parent->requestdata->inputs->firstname' not the property located at $obj->parent->requestdtaa->inputs->firstname`
I've tried various types of concatenation, use of var_export(), and others. I can explode it into an array and then loop the array like in this question.
But the variable '$property' can hold a value that goes 16 levels deep. And, the data I'm parsing can have hundreds of properties I need to import, so looping through and returning the value at each iteration until I get to level 16 X 100 items seems really inefficient; especially given that I know the actual location of the property at the start.
How do I get the value 'Travis' given (stdClass)$obj and (string)$property?
My initial searches didn't yield many results, however, after thinking up a broader range of search terms I found other questions on SO that addressed similar problems. I've come up with three solutions. All will work, but not all will work for everyone.
Solution 1 - Looping
Using an approach similar to the question referenced in my original question or the loop proposed by #miken32 will work.
Solution 2 - anonymous function
The string can be exploded into an array. The array can then be parsed using array_reduce() to produce the result. In my case, the working code (with a check for incorrect/non-existent property names/spellings) was this (PHP 7+):
//create object - this comes from and external API in my case, but I'll include it here
//so that others can copy and paste for testing purposes
$obj = (object)[
'parent' => (object)[
'requestdata' => (object)[
'inputs' => (object)[
'firstname' => 'Travis'
]
]
]
];
//string representing the property we want to get on the object
$property = 'parent->requestdata->inputs->firstname';
$name = array_reduce(explode('->', $property), function ($previous, $current) {
return is_numeric($current) ? ($previous[$current] ?? null) : ($previous->$current ?? null); }, $obj);
var_dump($name); //outputs Travis
see this question for potentially relevant information and the code I based my answer on.
Solution 3 - symfony property access component
In my case, it was easy to use composer to require this component. It allows access to properties on arrays and objects using simple strings. You can read about how to use it on the symfony website. The main benefit for me over the other options was the included error checking.
My code ended up looking like this:
//create object - this comes from and external API in my case, but I'll include it here
//so that others can copy and paste for testing purposes
//don't forget to include the component at the top of your class
//'use Symfony\Component\PropertyAccess\PropertyAccess;'
$obj = (object)[
'parent' => (object)[
'requestdata' => (object)[
'inputs' => (object)[
'firstname' => 'Travis'
]
]
]
];
//string representing the property we want to get on the object
//NOTE: syfony uses dot notation. I could not get standard '->' object notation to work.
$property = 'parent.requestdata.inputs.firstname';
//create symfony property access factory
$propertyAccessor = PropertyAccess::createPropertyAccessor();
//get the desired value
$name = $propertyAccessor->getValue($obj, $property);
var_dump($name); //outputs 'Travis'
All three options will work. Choose the one that works for you.
You're right that you'll have to do a loop iteration for each nested object, but you don't need to loop through "hundreds of properties" for each of them, you just access the one you're looking for:
$obj = (object)[
'parent' => (object)[
'requestdata' => (object)[
'inputs' => (object)[
'firstname' => 'Travis'
]
]
]
];
$property = "parent->requestdata->inputs->firstname";
$props = explode("->", $property);
while ($props && $obj !== null) {
$prop = array_shift($props);
$obj = $obj->$prop ?? null;
}
var_dump($obj);
Totally untested but seems like it should work and be fairly performant.
I am experiencing a problem where the way I am calling a PHP function is causing incorrect output from a a class that otherwise acts properly.
Specifically, I am using the full-name-parser package to split full names in to "first", "middle" and "last" parts. It performs this task accurately.
However, I am trying to do it inside WordPress - specifically during an import via the WPAllImport plugin in which I am importing CSV data as WordPress Users.
WPAllImport supports execution of PHP functions (standard or custom) on import input data, allowing it to be manipulated before saving, eg. [function_name({input_field[1]})].
So I have crafted a wrapper function, get_name_part to allow me to split my input field name_full in to first, middle and last names for import to those corresponding WordPress fields. My get_name_part looks like this...
require_once '/home/mysite/public_html/wp-content/plugins/cxt-user-wpai-import/full-name-parser/vendor/autoload.php';
use ADCI\FullNameParser\Parser;
// name eg. Jokubas Phillip Gardner
// part eg. title, first, middle, last, nick, suffix,
// called via eg. [get_name_part({name_full[1]}, "first")]
function get_name_part($name, $part) {
$parser = new Parser(
[
'part' => $part,
// 'fix_case' => FALSE,
'throws' => FALSE
]
);
// Return the parsed name part
$nameObject = $parser->parse($name);
// Give it back to WPAllImport
return $nameObject;
}
That is, it should take the source name name_full and also a string corresponding to full-name-parser options which describe the name part (either first, middle or last).
In WPAllImport, then, I am calling these three lines, separately, in the WPAllImport fields for first_name, last_name and my custom name_mid...
[get_name_part({name_full[1]}, "first")]
[get_name_part({name_full[1]}, "middle")]
[get_name_part({name_full[1]}, "last")]
In theory, this should allow me to use a single wrapper function to spit back the specified part from the specified full name.
The problem is...
The first operation completes successfully. That is, putting [get_name_part({name_full[1]}, "first")] in my import settings' first_name field successfully saves a parsed first name (eg. "Jokubas") in the first_name WordPress field.
However, things break down after that. [get_name_part({name_full[1]}, "last")] places no name at all in the last_name field. It repeatedly fails.
And [get_name_part({name_full[1]}, "middle")] places an incorrect combination of the actual middle and last (eg. "Phillip Gardner") as middle name.
So the actual output is:
First: Jokubas
Middle: Phillip Gardner
Last: [blank]
This is not consistent with full-name-parser itself, which correctly parses outside of my function and the import environment, like...
First: Jokubas
Middle: Phillip
Last: Gardner
I'm not sure what the reason is, but I feel like it may be something to do with calling the same function three times from the same import step, albeit with different parameters.
I appreciate that I have mentioned a couple of product names above which are not plain PHP. But I feel like there may something I could do in the function code to accommodate the fact that this function is getting called three times, with different parameters, in a single process; that the underlying reason may have something to do with this repetition or contamination.
I have, therefore, considered whether there is a need to destroy all variables at the end of the function. However, executing unset on $name, $part, $parser or $nameObject after the return does not fix anything.
What am I missing?
Thanks.
Edit:
WordPress debug.log shows:
[01-Mar-2018 15:43:06 UTC] WordPress database error Table 'context_wpdb.wpcxt_2_usermeta' doesn't exist for query SHOW COLUMNS FROM wpcxt_2_usermeta made by do_action('admin_init'), WP_Hook->do_action, WP_Hook->apply_filters, call_user_func_array, PMXI_Plugin->adminInit, PMXI_Admin_Manage->edit, PMXI_Admin_Import->template, PMXI_Model->setTable
[01-Mar-2018 15:43:06 UTC] WordPress database error Table 'context_wpdb.wpcxt_2_usermeta' doesn't exist for query SELECT umeta_id, meta_key FROM wpcxt_2_usermeta GROUP BY meta_key ORDER BY umeta_id made by do_action('admin_init'), WP_Hook->do_action, WP_Hook->apply_filters, call_user_func_array, PMXI_Plugin->adminInit, PMXI_Admin_Manage->edit, PMXI_Admin_Import->template, PMXI_Model_List->getBy
The PMXI* prefixes pertain to WPAllImport.
Edit 2:
Here are some valid ways in which full-name-parser operates on its own (ie. slices a supplied full name in to identified components)...
1.
Passing "part" as "all" (or not, since "all" is default) makes $nameObject an object containing all identified portions of the name...
require_once '/home/context/public_html/wp-content/plugins/cxt-user-wpai-import/full-name-parser/vendor/autoload.php';
use ADCI\FullNameParser\Parser;
$parser = new Parser(
[
'part' => 'all',
// 'fix_case' => FALSE,
'throws' => FALSE
]
);
$name = 'Jokubas Phillip Gardner';
$nameObject = $parser->parse($name);
print_r($nameObject);
... so the above outputs all available portions of the name...
FullNameParser\Name Object ( [leadingInitial:ADCI\FullNameParser\Name:private] => [firstName:ADCI\FullNameParser\Name:private] => Jokūbas [nicknames:ADCI\FullNameParser\Name:private] => [middleName:ADCI\FullNameParser\Name:private] => Phillip [lastName:ADCI\FullNameParser\Name:private] => Gardner [academicTitle:ADCI\FullNameParser\Name:private] => [suffix:ADCI\FullNameParser\Name:private] => [errors:ADCI\FullNameParser\Name:private] => Array ( ) )
That is correct behaviour.
2.
But passing only one portion of the name, eg. 'part' as 'last', like follows, makes $nameObject a string, containing only the one stated portion. So...
require_once '/home/context/public_html/wp-content/plugins/cxt-user-wpai-import/full-name-parser/vendor/autoload.php';
use ADCI\FullNameParser\Parser;
$parser = new Parser(
[
'part' => 'last',
// 'fix_case' => FALSE,
'throws' => FALSE
]
);
$name = 'Jokubas Phillip Gardner';
$nameObject = $parser->parse($name);
echo 'Here is my nameObject: ' . $nameObject . '<br />';
The above outputs the following only...
Gardner
That is also correct behaviour.
3.
Full-name-parser has named get* functions dedicated to obtaining individual portions, eg. getFirstName(), but they do not appear to be essential. I was attempting to use the part parameter instead.
Ok, the first attempt was wrong, but I think I found the problem.
This behavior happens if there is a space at the end of the name. Actually the parser can not find the last name in this case, and throws an exception, but you have turned exceptions off.
If you try
$parser = new ADCI\FullNameParser\Parser(['throws' => FALSE]);
var_dump($parser->parse('Jokubas Phillip Gardner '));
You will see:
class ADCI\FullNameParser\Name#2 (8) {
private $leadingInitial =>
NULL
private $firstName =>
string(7) "Jokubas"
private $nicknames =>
NULL
private $middleName =>
string(15) "Phillip Gardner"
private $lastName =>
NULL
private $academicTitle =>
NULL
private $suffix =>
NULL
private $errors =>
array(1) {
[0] =>
string(26) "Couldn't find a last name."
}
}
So the solution is to trim your input.
function get_name_part($name, $part) {
$parser = new Parser(
[
'part' => $part,
// 'fix_case' => FALSE,
// 'throws' => FALSE
]
);
// Return the parsed name part
return $parser->parse(trim($name));
}
'context_wpdb.wpcxt_2_usermeta' doesn't exist.
The obviuos in the error message is that there is a table missing or it is in the wrong schema.
How this impacts and if has to do with your issue it is difficult to know.
To help you troubble shoot the issue here there is a plugin that I took from some page some time ago.
This plugin will let you add log statemets to your code that will come out in the wp-content/debug.log file.
In this way you can inspect the different values for the functions you are calling and see what can be wrong.
For the plugin to work you have to set the following in wp_config.php:
define('WP_DEBUG', true);
define('WP_DEBUG_DISPLAY', false);
define('WP_DEBUG_LOG', true);
Once the plugin is installed, just add write_log($variable); in your code for it to come out in the debug.log file.
/* Plugin */
if(!defined('ABSPATH')) exit;
if ( ! function_exists('write_log')) {
function write_log ( $log ) {
if (defined('WP_DEBUG_LOG') && true === WP_DEBUG_LOG){
if ( is_array( $log ) || is_object( $log ) ) {
error_log( print_r( $log, true ) );
} else {
error_log( $log );
}
}
}
}
The plugin is meant to be put standalone in the wp-content/plugins folder.
When you are finished, remove the write_log() statements from your code, remove the plugin, and turn off the debug settings in wp_config.php
I search a lot in stack and google try to find the answer which seems to be easy but I'm still stuck with it
I write a code to encode json with values I wanted from . and I would like to add a key / value to the JSON
the JSON is as following structure
{
- files: [
{
title: "works",
- tracks: [
{
title: "File",
format: "mp3"
}
]
},
-{
title: "season1",
tracks: [
{
title: "Button-1",
format: "wav"
},
-{
title: "Beep-9",
format: "wav"
}
]
}
]
}
I want to add to that a key and its value at the beginning to the json as properties under the title files , I mean that can be read by code as
json[files][new_key]
I tried to set that value like this
$json['new_key'] = "new_value";
but this causes adding numbers to the arrays in json , I don't why they numbered
this numbers affect my reading way of the json as JSONModel in my iOS app
so , I hope you can help me
thanks in advance
Assuming that the new value you want to add varies by file, you would need to loop through $json[files] and insert them per key/value pair.
<?php
for($i=0; $i<count($json); $i++)
$json[files][$i]["new_key"] = "value";
?>
I'm still not sure what you have exactly, but it seems you are trying to manipulate the json string.
If done correctly, that is probably the most efficient solution, but what you could also do is:
use json_decode to generate an array from your json string;
locate the correct section / sub-array where you want to add your data;
use array_unshift to prepend your new key - value pair;
use json_encode to generate a json string from your complete array.
The reason you're getting numbers appearing is because you're adding a key to an array (which functions more or less as a list in JS). So before you basically have the object "files" as a list of objects zero-indexed like any other JS array. When you add the key, JS simply adds your key to the end of your present keys (in your case 0 and 1).
It seems like you have a list of multimedia objects where each has a title and a list of tracks. The most straightforward way to solve your issue would be as follows:
$fileItem['title'] = 'works';
$fileItem['tracks'] = array(
array(
'title' => 'File',
'format' => 'mp3'
)
);
$json['files'][] = $fileItem;
$fileItem['title'] = 'season1';
$fileItem['tracks'] = array(
array(
'title' => 'Button-1',
'format' => 'wav'
),
array(
'title' => 'Beep-9',
'format' => 'wav'
)
);
$json['files'][] = $fileItem;
Then you JSON encode it and return it as you normally would. You can put the above in a loop as well. I lack enough context to recommend exactly how.
We have a web application that does time-tracking, payroll, and HR. As a result, we have to write a lot of fixed-width data files for export into other systems (state tax filings, ACH files, etc). Does anyone know of a good library for this where you can define the record types/structures, and then act on them in an OOP paradigm?
The idea would be a class that you hand specifications, and then work with an instance of said specification. IE:
$icesa_file = new FixedWidthFile();
$icesa_file->setSpecification('icesa.xml');
$icesa_file->addEmployer( $some_data_structure );
Where icesa.xml is a file that contains the spec, although you could just use OOP calls to define it yourself:
$specification = new FixedWidthFileSpecification('ICESA');
$specification->addRecordType(
$record_type_name = 'Employer',
$record_fields = array(
array('Field Name', Width, Vailditation Type, options)
)
);
EDIT: I'm not looking for advice on how to write such a library--I just wanted to know if one already existed. Thank you!!
I don't know of a library that does exactly what you want, but it should be rather straight-forward to roll your own classes that handle this. Assuming that you are mainly interested in writing data in these formats, I would use the following approach:
(1) Write a lightweight formatter class for fixed width strings. It must support user defined record types and should be flexible with regard to allowed formats
(2) Instantiate this class for every file format you use and add required record types
(3) Use this formatter to format your data
As you suggested, you could define the record types in XML and load this XML file in step (2). I don't know how experienced you are with XML, but in my experience XML formats often causes a lot of headaches (probably due to my own incompetence regarding XML). If you are going to use these classes only in your PHP program, there's not much to gain from defining your format in XML. Using XML is a good option if you will need to use the file format definitions in many other applications as well.
To illustrate my ideas, here is how I think you would use this suggested formatter class:
<?php
include 'FixedWidthFormatter.php' // contains the FixedWidthFormatter class
include 'icesa-format-declaration.php' // contains $icesaFormatter
$file = fopen("icesafile.txt", "w");
fputs ($file, $icesaFormatter->formatRecord( 'A-RECORD', array(
'year' => 2011,
'tein' => '12-3456789-P',
'tname'=> 'Willie Nelson'
)));
// output: A2011123456789UTAX Willie Nelson
// etc...
fclose ($file);
?>
The file icesa-format-declaration.php could contain the declaration of the format somehow like this:
<?php
$icesaFormatter = new FixedWidthFormatter();
$icesaFormatter->addRecordType( 'A-RECORD', array(
// the first field is the record identifier
// for A records, this is simply the character A
'record-identifier' => array(
'value' => 'A', // constant string
'length' => 1 // not strictly necessary
// used for error checking
),
// the year is a 4 digit field
// it can simply be formatted printf style
// sourceField defines which key from the input array is used
'year' => array(
'format' => '% -4d', // 4 characters, left justified, space padded
'length' => 4,
'sourceField' => 'year'
),
// the EIN is a more complicated field
// we must strip hyphens and suffixes, so we define
// a closure that performs this formatting
'transmitter-ein' => array(
'formatter'=> function($EIN){
$cleanedEIN = preg_replace('/\D+/','',$EIN); // remove anything that's not a digit
return sprintf('% -9d', $cleanedEIN); // left justified and padded with blanks
},
'length' => 9,
'sourceField' => 'tein'
),
'tax-entity-code' => array(
'value' => 'UTAX', // constant string
'length' => 4
),
'blanks' => array(
'value' => ' ', // constant string
'length' => 5
),
'transmitter-name' => array(
'format' => '% -50s', // 50 characters, left justified, space padded
'length' => 50,
'sourceField' => 'tname'
),
// etc. etc.
));
?>
Then you only need the FixedWidthFormatter class itself, which could look like this:
<?php
class FixedWidthFormatter {
var $recordTypes = array();
function addRecordType( $recordTypeName, $recordTypeDeclaration ){
// perform some checking to make sure that $recordTypeDeclaration is valid
$this->recordTypes[$recordTypeName] = $recordTypeDeclaration;
}
function formatRecord( $type, $data ) {
if (!array_key_exists($type, $this->recordTypes)) {
trigger_error("Undefinded record type: '$type'");
return "";
}
$output = '';
$typeDeclaration = $this->recordTypes[$type];
foreach($typeDeclaration as $fieldName => $fieldDeclaration) {
// there are three possible field variants:
// - constant fields
// - fields formatted with printf
// - fields formatted with a custom function/closure
if (array_key_exists('value',$fieldDeclaration)) {
$value = $fieldDeclaration['value'];
} else if (array_key_exists('format',$fieldDeclaration)) {
$value = sprintf($fieldDeclaration['format'], $data[$fieldDeclaration['sourceField']]);
} else if (array_key_exists('formatter',$fieldDeclaration)) {
$value = $fieldDeclaration['formatter']($data[$fieldDeclaration['sourceField']]);
} else {
trigger_error("Invalid field declaration for field '$fieldName' record type '$type'");
return '';
}
// check if the formatted value has the right length
if (strlen($value)!=$fieldDeclaration['length']) {
trigger_error("The formatted value '$value' for field '$fieldName' record type '$type' is not of correct length ({$fieldDeclaration['length']}).");
return '';
}
$output .= $value;
}
return $output . "\n";
}
}
?>
If you need read support as well, the Formatter class could be extended to allow reading as well, but this might be beyond the scope of this answer.
I have happily used this class for similar use before. It is a php-classes file, but it is very well rated and has been tried-and-tested by many. It is not new (2003) but regardless it still does a very fine job + has a very decent and clean API that looks somewhat like the example you posted with many other goodies added.
If you can disregard the german usage in the examples, and the age factor -> it is very decent piece of code.
Posted from the example:
//CSV-Datei mit Festlängen-Werten
echo "<p>Import aus der Datei fixed.csv</p>";
$csv_import2 = new CSVFixImport;
$csv_import2->setFile("fixed.csv");
$csv_import2->addCSVField("Satzart", 2);
$csv_import2->addCSVField("Typ", 1);
$csv_import2->addCSVField("Gewichtsklasse", 1);
$csv_import2->addCSVField("Marke", 4);
$csv_import2->addCSVField("interne Nummer", 4);
$csv_import2->addFilter("Satzart", "==", "020");
$csv_import2->parseCSV();
if($csv_import->isOK())
{
echo "Anzahl der Datensätze: <b>" . $csv_import2->CSVNumRows() . "</b><br>";
echo "Anzahl der Felder: <b>" . $csv_import2->CSVNumFields() . "</b><br>";
echo "Name des 1.Feldes: <b>" . $csv_import2->CSVFieldName(0) . "</b><br>";
$csv_import2->dumpResult();
}
My 2 cents, good-luck!
I don't know of any PHP library that specifically handles fixed-width records. But there are some good libraries for filtering and validating a row of data fields if you can do the job of breaking up each line of the file yourself.
Take a look at the Zend_Filter and Zend_Validate components from Zend Framework. I think both components are fairly self-contained and require only Zend_Loader to work. If you want you can pull just those three components out of Zend Framework and delete the rest of it.
Zend_Filter_Input acts like a collection of filters and validators. You define a set of filters and validators for each field of a data record which you can use to process each record of a data set. There are lots of useful filters and validators already defined and the interface to write your own is pretty straightforward. I suggest the StringTrim filter for removing padding characters.
To break up each line into fields I would extend the Zend_Filter_Input class and add a method called setDataFromFixedWidth(), like so:
class My_Filter_Input extends Zend_Filter_Input
{
public function setDataFromFixedWidth($record, array $recordRules)
{
if (array_key_exists('regex', $recordRules) {
$recordRules = array($recordRules);
}
foreach ($recordRules as $rule) {
$matches = array();
if (preg_match($rule['regex'], $record, $matches)) {
$data = array_combine($rule['fields'], $matches);
return $this->setData($data);
}
}
return $this->setData(array());
}
}
And define the various record types with simple regular expressions and matching field names. ICESA might look something like this:
$recordRules = array(
array(
'regex' => '/^(A)(.{4})(.{9})(.{4})/', // This is only the first four fields, obviously
'fields' => array('recordId', 'year', 'federalEin', 'taxingEntity',),
),
array(
'regex' => '/^(B)(.{4})(.{9})(.{8})/',
'fields' => array('recordId', 'year', 'federalEin', 'computer',),
),
array(
'regex' => '/^(E)(.{4})(.{9})(.{9})/',
'fields' => array('recordId', 'paymentYear', 'federalEin', 'blank1',),
),
array(
'regex' => '/^(S)(.{9})(.{20})(.{12})/',
'fields' => array('recordId', 'ssn', 'lastName', 'firstName',),
),
array(
'regex' => '/^(T)(.{7})(.{4})(.{14})/',
'fields' => array('recordId', 'totalEmployees', 'taxingEntity', 'stateQtrTotal'),
),
array(
'regex' => '/^(F)(.{10})(.{10})(.{4})/',
'fields' => array('recordId', 'totalEmployees', 'totalEmployers', 'taxingEntity',),
),
);
Then you can read your data file line by line and feed it into the input filter:
$input = My_Filter_Input($inputFilterRules, $inputValidatorRules);
foreach (file($filename) as $line) {
$input->setDataFromFixedWidth($line, $recordRules);
if ($input->isValid()) {
// do something useful
}
else {
// scream and shout
}
}
To format data for writing back to the file, you would probably want to write your own StringPad filter that wraps the internal str_pad function. Then for each record in your data set:
$output = My_Filter_Input($outputFilterRules);
foreach ($dataset as $record) {
$output->setData($record);
$line = implode('', $output->getEscaped()) . "\n";
fwrite($outputFile, $line);
}
Hope this helps!
I think you need a bit more information than you supplied:
What kind of data structures would you like to use for your records and column definitions?
It seems like this is a rather specialized class that would require customization for your specific use case.
I have a PHP class that I wrote that basically does what you are looking for, but relying on other classes that we use in our system. If you can supply the types of data structures you want to use it with I can check if it will work for you and send it over.
Note: I published this answer before from a public computer and I could not get it to appear to be from me (it showed as some random user). If you see it, please ignore the answer from 'john'.
If this is text file with separated fields, - your will need write it yourself.
Probably it is not a large problem. Good organization, will save a lot of time.
Your need universal way of defining structures. I.e. xml.
Your need something to generate ... specially I prefer an Smarty templating for this.
So this one:
<group>
<entry>123</entry>
<entry>123</entry>
<entry>123</entry>
</group>
Can be easy interpreted into test with this template:
{section name=x1 loop=level1_arr}
{--output root's--}
{section name=x2 loop=level1_arr[x1].level2_arr}
{--output entry's--}
{/section}
{/section}
This is just idea.
But imagine:
You need xml
You need template
i.e. 2 definitions to abstract any text structure
Perhaps the dbase functions are what you want to use. They are not OOP, but it probably would not be too difficult to build a class that would act on the functions provided in the dbase set.
Take a look at the link below for details on dbase functionality available in PHP. If you're just looking to create a file for import into another system, these functions should work for you. Just make sure you pay attention to the warnings. Some of the key warnings are:
There is no support for indexes or memo fields.
There is no support for locking.
Two concurrent web server processes modifying the same dBase file will very likely ruin your database.
http://php.net/manual/en/book.dbase.php
I'm sorry i cant help you with a direct class i have seen some thing that does this but i can't remember where so sorry for that but it should be simple for a coder to build,
So how i have seen this work in an example:
php reads in data
php then uses a flag (E.G a $_GET['type']) to know how to output the data E.G Printer, HTML, Excel
So you build template files for each version then depending on the flag you load and use the defined template, as for Fixed Width this is a HTML thing not PHP so this should be done in templates CSS
Then from this you can output your data how ever any user requires it,
Smarty Templates is quite good for this and then the php header to send the content type when required.