You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

136 lines
5.9 KiB

<?php
/**
* TablePress Formula Evaluation PHPSpreadsheet Class.
*
* @package TablePress
* @subpackage Formulas
* @author Tobias Bäthge
* @since 2.0.0
*/
// Prohibit direct script loading.
defined( 'ABSPATH' ) || die( 'No direct script access allowed!' );
/**
* TablePress Formula Evaluation PHPSpreadsheet Class
*
* @package TablePress
* @subpackage Formulas
* @author Tobias Bäthge
* @since 2.0.0
*/
class TablePress_Evaluate_PHPSpreadsheet {
/**
* Initializes the Formula Evaluation PHPSpreadsheet class.
*
* @since 2.0.0
*/
public function __construct() {
// Load PHPSpreadsheet via the Composer autoloading mechanism.
TablePress::load_file( 'autoload.php', 'libraries' );
}
/**
* Evaluates formulas in the passed table.
*
* @since 1.0.0
*
* @param array<int, array<int, string>> $table_data Table data in which formulas shall be evaluated.
* @param string $table_id ID of the passed table.
* @return array<int, array<int, string>> Table data with evaluated formulas.
*/
public function evaluate_table_data( array $table_data, string $table_id ): array {
$table_has_formulas = false;
// Loop through all cells to check for formulas and convert notations.
foreach ( $table_data as &$row ) {
foreach ( $row as &$cell_content ) {
if ( '' === $cell_content || '=' === $cell_content || '=' !== $cell_content[0] ) {
continue;
}
$table_has_formulas = true;
// Convert legacy "formulas in text" notation (`=Text {A3+B3} Text`) to standard Excel notation (`="Text "&A3+B3&" Text"`).
if ( 1 === preg_match( '#{(.+?)}#', $cell_content ) ) {
$cell_content = str_replace( '"', '""', $cell_content ); // Preserve existing quotation marks in text around formulas.
$cell_content = '="' . substr( $cell_content, 1 ) . '"'; // Wrap the whole cell content in quotation marks, as there will be text around formulas.
$cell_content = (string) preg_replace( '#{(.+?)}#', '"&$1&"', $cell_content, -1, $count ); // Convert all wrapped formulas to standard Excel notation.
}
}
}
unset( $row, $cell_content ); // Unset use-by-reference parameters of foreach loops.
// No need to use the PHPSpreadsheet Calculation engine if the table does not contain formulas.
if ( ! $table_has_formulas ) {
return $table_data;
}
try {
$spreadsheet = new \TablePress\PhpOffice\PhpSpreadsheet\Spreadsheet();
$worksheet = $spreadsheet->setActiveSheetIndex( 0 );
$worksheet->fromArray( /* $source */ $table_data, /* $nullValue */ '' );
// Don't allow cyclic references.
TablePress\PhpOffice\PhpSpreadsheet\Calculation\Calculation::getInstance( $spreadsheet )->cyclicFormulaCount = 0;
/*
* Register variables as Named Formulas.
* The variables `ROW`, `COLUMN`, `CELL`, `PI`, and `E` should be considered deprecated and only their formulas should be used.
*/
$spreadsheet->addNamedFormula( new \TablePress\PhpOffice\PhpSpreadsheet\NamedFormula( 'TABLE_ID', $worksheet, $table_id ) );
$num_rows = (string) count( $table_data );
$spreadsheet->addNamedFormula( new \TablePress\PhpOffice\PhpSpreadsheet\NamedFormula( 'NUM_ROWS', $worksheet, $num_rows ) );
$num_columns = (string) count( $table_data[0] );
$spreadsheet->addNamedFormula( new \TablePress\PhpOffice\PhpSpreadsheet\NamedFormula( 'NUM_COLUMNS', $worksheet, $num_columns ) );
$spreadsheet->addNamedFormula( new \TablePress\PhpOffice\PhpSpreadsheet\NamedFormula( 'ROW', $worksheet, '=ROW()' ) );
$spreadsheet->addNamedFormula( new \TablePress\PhpOffice\PhpSpreadsheet\NamedFormula( 'COLUMN', $worksheet, '=COLUMN()' ) );
$spreadsheet->addNamedFormula( new \TablePress\PhpOffice\PhpSpreadsheet\NamedFormula( 'CELL', $worksheet, '=ADDRESS(ROW(),COLUMN(),4)' ) );
$spreadsheet->addNamedFormula( new \TablePress\PhpOffice\PhpSpreadsheet\NamedFormula( 'PI', $worksheet, '=PI()' ) );
$spreadsheet->addNamedFormula( new \TablePress\PhpOffice\PhpSpreadsheet\NamedFormula( 'E', $worksheet, '=EXP(1)' ) );
// Loop through all table cells and replace formulas with evaluated values.
$cell_collection = $worksheet->getCellCollection();
foreach ( $table_data as $row_idx => &$row ) {
foreach ( $row as $column_idx => &$cell_content ) {
if ( strlen( $cell_content ) > 1 && '=' === $cell_content[0] ) {
// Adapted from \TablePress\PhpOffice\PhpSpreadsheet\Worksheet\Worksheet::rangeToArray().
$cell_reference = \TablePress\PhpOffice\PhpSpreadsheet\Cell\Coordinate::stringFromColumnIndex( $column_idx + 1 ) . ( $row_idx + 1 );
if ( $cell_collection->has( $cell_reference ) ) {
$cell = $cell_collection->get( $cell_reference );
try {
$cell_content = (string) $cell->getCalculatedValue();
// Convert hyperlinks, e.g. generated via `=HYPERLINK()` to HTML code.
$cell_has_hyperlink = $worksheet->hyperlinkExists( $cell_reference ) && ! $worksheet->getHyperlink( $cell_reference )->isInternal();
if ( $cell_has_hyperlink ) {
$url = $worksheet->getHyperlink( $cell_reference )->getUrl();
if ( '' !== $url ) {
$url = esc_url( $url );
$cell_content = "<a href=\"{$url}\">{$cell_content}</a>";
}
}
} catch ( \TablePress\PhpOffice\PhpSpreadsheet\Calculation\Exception $exception ) {
$message = str_replace( 'Worksheet!', '', $exception->getMessage() );
$cell_content = "!ERROR! {$message}";
}
}
}
}
}
unset( $row, $cell_content ); // Unset use-by-reference parameters of foreach loops.
// Save PHP memory.
$spreadsheet->disconnectWorksheets();
unset( $cell_collection, $worksheet, $spreadsheet );
} catch ( \TablePress\PhpOffice\PhpSpreadsheet\Calculation\Exception $exception ) {
$message = str_replace( 'Worksheet!', '', $exception->getMessage() );
$table_data = array( array( "!ERROR! {$message}" ) );
}
return $table_data;
}
} // class TablePress_Evaluate_PHPSpreadsheet