: str_replace(): Passing null to parameter #2 ($replace) of type array|string is deprecated in
namespace NinjaForms\Includes\Handlers;
use NinjaForms\Includes\Entities\SubmissionField;
use NinjaForms\Includes\Entities\SingleSubmission;
use NinjaForms\Includes\Handlers\SubmissionAggregate;
use NinjaForms\Includes\Handlers\Field;
* Adapts SubmissionAggregate to provide data required by CSV Export
* CSV Export requires specific structuring of submission data, too specialized
* to be included in the SubmissionAggregate. This class structures submission
* data as needed for CSV export
class SubmissionAggregateCsvExportAdapter
* Field labels, keyed on field key
* Admin field labels, keyed on field key
* Indexed array of field types to be omitted in output
protected $hiddenFieldTypes;
* Array of field types keyed on field key
protected $fieldTypes = [];
* Array of field Ids keyed on field key
protected $fieldIds = [];
* Array of column values keyed on field key
* Property is reset and reused for each request; column count must equal
* that of $labels, $adminLabels, $fieldtypes, and $fieldIds
protected $columnValues = [];
* @var SubmissionAggregate
public $submissionAggregate;
* Construct with SubmissionAggregate
* Lookup array properties are not populated until the first request for
* either fieldTypes, label/adminLable, or fieldIds.
* @param SubmissionAggregate $submissionAggregate
public function __construct(SubmissionAggregate $submissionAggregate)
$this->submissionAggregate = $submissionAggregate;
* Return array of field labels keyed on field keys
* If hiddenFieldTypes array is set, labels filtered to hide those types
* @param bool $useAdminLabels Optionally use admin_labels
public function getLabels(?bool $useAdminLabels = false): array
if (!isset($this->labels)) {
$this->constructFieldLookups();
$return = $this->adminLabels;
* Return array of field types keyed on field keys
public function getFieldTypes(): array
if (!isset($this->fieldTypes)) {
$this->constructFieldLookups();
return $this->fieldTypes;
* Return array of field Ids keyed on field keys
public function getFieldIds(): array
if (!isset($this->fieldIds)) {
$this->constructFieldLookups();
* Return array of submission Ids in the collection
* Generated at time of request to ensure it is up to date after last
public function getSubmissionIds(): array
/** @var SingleSubmission $singleSubmission */
foreach ($this->submissionAggregate->getAggregatedSubmissions() as $singleSubmission) {
$idArray[] = $singleSubmission->getSubmissionRecordId();
* Get column values for a given submission aggregated key
* @param string $aggregatedKey
public function getColumnValuesByAggregatedKey(string $aggregatedKey): array
$this->columnValues = [];
$singleSubmission = $this->submissionAggregate->getSubmissionValuesByAggregatedKey($aggregatedKey);
$populatedfieldDefinitionCollection = $singleSubmission->getSubmissionFieldCollection();
foreach($populatedfieldDefinitionCollection as $populatedSubmissionField){
$this->extractSubmissionFieldData($populatedSubmissionField,false);
return $this->columnValues;
* Construct labels/adminLabels from submission aggregate
protected function constructFieldLookups(): void
$fieldDefinitionCollection = $this->submissionAggregate->getFieldDefinitionCollection();
/** @var SubmissionField $submissionField */
foreach ($fieldDefinitionCollection as $submissionField) {
$this->extractSubmissionFieldData($submissionField, true);
* Given submission field, extract labels + meta, or submission values
* @param SubmissionField $submissionField
* @param boolean|null $labelsOnly
protected function extractSubmissionFieldData(SubmissionField $submissionField, ?bool $labelsOnly = false): void
// omit from collection if type is part of hidden field type collection
isset($this->hiddenFieldTypes) &&
in_array($submissionField->getType(), $this->hiddenFieldTypes)
if ('repeater' === $submissionField->getType()) {
$this->extractRepeaterFieldColumns($submissionField, $labelsOnly);
$this->setFieldMetaData($submissionField);
$this->setFieldLabels($submissionField);
$key = $submissionField->getSlug();
$key=$submissionField->getId();
$rawValue = $submissionField->getValue();
$nfField = $this->convertSubmissionFieldToNfField($submissionField);
$filteredValue = $this->filterRawValue($key,$rawValue, $nfField);
if(\is_array($filteredValue)){
$filteredValue = \implode(',',$filteredValue);
$this->setColumnValue($key,$filteredValue);
* Construct NF Field from SubmissionField
* NF Field is needed to apply existing NF filters
* @param SubmissionField $submissionField
protected function convertSubmissionFieldToNfField(SubmissionField $submissionField):Field
$nfField = Field::fromArray(
'id'=>$submissionField->getId(),
'type'=>$submissionField->getType(),
'settings'=>$submissionField->getOriginal() ]
* Extract repeater field column headers
* @todo Enable external setting of in-CSV delimiter
* @todo ~L258 - adjust deconstructed value to handle listmultiselect arrays (other fields may also have arrays)
* @param SubmissionField $submissionField
protected function extractRepeaterFieldColumns(SubmissionField $submissionField, bool $labelsOnly): void
$repeaterFieldsCollection =$this->extractRepeaterFieldsFromSubmisionField($submissionField);
if (empty($repeaterFieldsCollection)) {
$deconstructedValues = $this->deconstructRepeaterFieldValue((array)$submissionField->getValue());
// iterate each SubmissionField within the repeater fields collection
foreach ($repeaterFieldsCollection as $repeaterField) {
if( !in_array($repeaterField->type, $this->hiddenFieldTypes) ){
$this->setFieldMetaData($repeaterField);
$this->setFieldLabels($repeaterField);
$nfField = $this->convertSubmissionFieldToNfField($repeaterField);
$id = $repeaterField->getId();
$key = $repeaterField->getSlug();
$key=$repeaterField->getId();
if(isset($deconstructedValues[$id])){
foreach($deconstructedValues[$id] as $rawRepeatedValueArray){
$rawRepeatedValue = $rawRepeatedValueArray['value'];
$filteredValue = $this->filterRawValue($key,$rawRepeatedValue, $nfField);
if(\is_array($filteredValue)){
$filteredValue = \implode(',',$filteredValue);
$filteredValues[] = $filteredValue;
//Construct an array of rows instead of column value if repeater was repeated (index that array by "repeater" for later reference)
if( count($filteredValues) > 0){
$this->setColumnValue("repeater", [$key, $filteredValues]);
$this->setColumnValue($key, $filteredValue);
* Extracts collection of SubmissionFields within a fieldset repeater
* @param SubmissionField $submissionField
* @return SubmissionField[]
protected function extractRepeaterFieldsFromSubmisionField(SubmissionField $submissionField): array
$repeaterFieldsArray = $submissionField->getFieldsetRepeaterFields();
$keyedFieldSettings = $this->constructRepeaterFieldSettingsLookup($submissionField);
// iterate each SubmissionField within the repeater fields collection
foreach ($repeaterFieldsArray as $repeaterFieldArray) {
if (isset($repeaterFieldArray['key'])) {
$repeaterFieldArray['slug'] = $repeaterFieldArray['key'];
unset($repeaterFieldArray['key']);
if (isset($repeaterFieldArray['admin_label'])) {
$repeaterFieldArray['adminLabel'] = $repeaterFieldArray['admin_label'];
unset($repeaterFieldArray['admin_label']);
$id = isset($repeaterFieldArray)?$repeaterFieldArray['id']:'';
if (isset($keyedFieldSettings[$id])) {
$repeaterFieldArray['original'] = $keyedFieldSettings[$id];
$repeaterFieldArray['original'] = [];
$repeaterField = SubmissionField::fromArray($repeaterFieldArray);
$return[] = $repeaterField;
* Construct lookup of field settings for fields within fieldset repeater field
* @param SubmissionField $submissionField
protected function constructRepeaterFieldSettingsLookup(SubmissionField $submissionField): array
$keyedFieldSettings = [];
$originalFieldSettings = $submissionField->getOriginal();
if (!empty($originalFieldSettings['fields'])) {
$nestedFieldKeys = \array_column($originalFieldSettings['fields'], 'id');
$keyedFieldSettings = \array_combine($nestedFieldKeys, $originalFieldSettings['fields']);
return $keyedFieldSettings;
* Ensure each repeater field value is a string
* @param SubmissionField $submissionField
* @param array $deconstructedValue
* @param string $repeatedValueDelimiter
protected function getStringedValue(SubmissionField $submissionField, array $deconstructedValue, string $repeatedValueDelimiter): string
$arrayTypes =['listmultiselect'];
$valueColumn = \array_column( $deconstructedValue,'value');
if(\in_array($submissionField->getType(),$arrayTypes)){
foreach($valueColumn as &$value){
$value=implode(',',$value);
$return = implode($repeatedValueDelimiter,$valueColumn);
* Deconstruct repeater field array by repeated fields
* @todo Add exception handling for unexpected key structure
* @param array $constructedValue
protected function deconstructRepeaterFieldValue(array $constructedValue): array
foreach ($constructedValue as $constructedKey => $submissionValue) {
$exploded = explode($delimiter,$constructedKey);
$return[$exploded[0]][$exploded[1]]=$submissionValue;
* Add key value lookups for labels and admin labels
* @param string $adminLabel
protected function setFieldLabels(SubmissionField $submissionField): void
$slug = $submissionField->getSlug();
$slug=$submissionField->getId();
$label = $submissionField->getLabel();
$adminLabel = $submissionField->getAdminLabel();
$this->labels[$slug] = \WPN_Helper::maybe_escape_csv_column( $label );
if ('' !== $adminLabel) {
$this->adminLabels[$slug] = \WPN_Helper::maybe_escape_csv_column( $adminLabel );
// set adminLabel default as field label
$this->adminLabels[$slug] = $this->labels[$slug];
* Add key value lookups for Id and type on all fields in collection
* This includes hidden fields and parent fieldset repeater fields
* @param SubmissionField $submissionField
protected function setFieldMetaData(SubmissionField $submissionField): void
$slug = $submissionField->getSlug();
$slug = $submissionField->getId();
$this->fieldTypes[$slug] = $submissionField->getType();
$this->fieldIds[$slug] = $submissionField->getId();
* Set column value for a given field