0

I'm working on an Export method in Laravel to generate an XLSX file with included formulae. Everything is working, but on Export, the formula is getting an extra @ symbol added in a couple places, which then breaks the formula... The cell value is rendered as #VALUE.

This is the function:

namespace App\Exports\Reports;

use App\Exports\Sheets\Reports\SheetOne;
use App\Exports\Sheets\Reports\SheetTwo;
use App\Exports\Sheets\Reports\SheetThree;
use Maatwebsite\Excel\Concerns\WithMultipleSheets;

class ExcelReport implements WithMultipleSheets {
  public function sheets(): array {
    return [
      new SheetOne($this->sheetOneData),
      new SheetTwo($this->sheetTwoData),
      new SheetThree($this->sheetThreeData)
    ];
  }
}

The issue is on "Sheet 2":

namespace App\Exports\Sheets\Reports;

use App\Models\SheetTwoData;
use Illuminate\Support\Collection;
use Maatwebsite\Excel\Concerns\WithTitle;
use Maatwebsite\Excel\Concerns\WithMapping;
use Maatwebsite\Excel\Concerns\WithHeadings;
use Maatwebsite\Excel\Concerns\FromCollection;

class SheetTwo implements FromCollection, WithHeadings, WithMapping, WithTitle {
  protected Collection $sheetTwoData;
  private int $maxIndex;

  public function __construct(Collection $sheetTwoData) {
    $this->sheetTwoData = $sheetTwoData;
    $this->maxIndex = 2 + $sheetTwoData->count() - 1;
  }

  public function collection(): collection {
    $calcOne = "SheetTwo!D2:D{$this->maxIndex}";
    $calcTwo = "SheetTwo!G2:G{$this->maxIndex}";

    return $this->sheetTwoData->push([
      '',
      'Totals',
      "=SUBTOTAL(109, C2:C{$this->maxIndex})",
      "=SUM(IF({$calcOne}<>\"\",1/COUNTIF({$calcOne},{$calcOne}),0))",
      "=SUM(IF({$calcTwo}<>\"\",1/COUNTIF({$calcTwo},{$calcTwo}),0))",
      ''
    ]);
  }

  public function headings(): array {
    return [
      'Column A',
      'Column B',
      'Column C',
      'Column D',
      'Column E',
      'Column F',
      'Column G'
    ];
  }

  public function map($row): array {
    if ($row instanceof SheetTwoData) {
      return $row->toArray();
    }

    return $row;
  }

  public function title(): string {
    return 'SheetTwo';
  }

This is then hooked up to a POST Form via a Button, which when clicked, streams the generated XLSX as a Download through Chrome (or whatever browser):

if ($request->method() == 'POST') {
  return Excel::download(new ExcelReport([
    'sheetOneData' => SheetOneData::all(),
    'sheetTwoData' => SheetTwoData::all(),
    'sheetThreeData' => SheetThreeData::all()
  ]), 'excel-report.xlsx');
}

This all works just fine, and the file is downloaded, but the $calcOne and $calcTwo strings get adjusted. If I add dd($row); before return $row; (the handler that renders the row with these formulae), I see this:

array:6 [▼
  0 => ""
  1 => "Totals"
  2 => "=SUBTOTAL(109, C2:C50)"
  3 => "=SUM(IF(SheetTwo!D2:D50<>"",1/COUNTIF(SheetTwo!D2:D50,SheetTwo!D2:D50),0))"
  4 => "=SUM(IF(SheetTwo!G2:G50<>"",1/COUNTIF(SheetTwo!G2:G50,SheetTwo!G2:G50),0))"
  5 => ""
]

However, in the XLSX file, these formulae render as:

enter image description here

=SUM(IF(@SheetTwo!D2:D50<>"",1/COUNTIF(SheetTwo!D2:D50,@SheetTwo!D2:D50),0))

($calcTwo, targeting G2:G50 is also broken in the same way).

Has anyone seen this issue before? Google is being less than helpful since searching for @ or "at symbol" is very ambiguous, and Excel's help is laughably bad. Thanks!

Sidenote: If I manually remove this @ symbol from the generated XLSX file, then the formulae work as intended. The whole point of this XLSX file is so that I can generate it and send via Email, so currently I can modify it before send, but it might be fully automated in the future.

Tim Lewis
  • 27,813
  • 13
  • 73
  • 102

1 Answers1

1

I'm not sure why that occurs but maatwebsite/excel is just a wrapper for phpoffice/phpspreadsheet. Whenever I've had to include formulas, I do so by getting the underlying Phpspreadsheet instance through events.

A problem though, is the methods are static so you can't really access $this->maxIndex without using an ugly hack.

namespace App\Exports\Sheets\Reports;

use Illuminate\Support\Collection;
use Maatwebsite\Excel\Concerns\{FromCollection, RegistersEventListeners, WithEvents, WithHeadings, WithMapping, WithTitle};
use Maatwebsite\Excel\Events\AfterSheet;

class SheetTwo implements FromCollection, WithEvents, WithHeadings, WithMapping, WithTitle,
{
    use RegistersEventListeners;

    protected Collection $sheetTwoData;

    public function __construct(Collection $sheetTwoData)
    {
        $this->sheetTwoData = $sheetTwoData;
    }

    public function collection(): Collection
    {
        return $this->sheetTwoData;
    }

    ...

    public static function afterSheet(AfterSheet $event)
    {
        // $event->sheet returns \Maatwebsite\Excel\Sheet which has all the methods of \PhpOffice\PhpSpreadsheet\Worksheet\Worksheet
        $highestRow = $event->sheet->getHighestRow();
        $newRow = $highestRow + 1; 

        $event->sheet->setCellValue('B' . $newRow, 'Totals');
        $event->sheet->setCellValue('C' . $newRow, "=SUBTOTAL(109, C2:C{})");
        $event->sheet->setCellValue('D' . $newRow, "=SUM(IF(D2:D{$highestRow}<>\"\",1/COUNTIF(D2:D{$highestRow},D2:D{$highestRow}),0))");
        $event->sheet->setCellValue('E' . $newRow, "=SUM(IF(G2:G{$highestRow}<>\"\",1/COUNTIF(G2:G{$highestRow},G2:G{$highestRow}),0))");
    }
}

Considering these are formulas on the SheetTwo worksheet, you shouldn't need to specify SheetTwo! in their values.

IGP
  • 14,160
  • 4
  • 26
  • 43
  • Yeah, I was hoping to avoid having to use the `afterSheet` Callback (or others), since it seemed pretty straight-forward to simply use the PHP variables for these formulae, but I suspect I'll have to go this route. And yeah, the `SheetTwo! ...` was a carryover from my multiple attempts to fix this; it is redundant if specifying cell ranges in the current Sheet. I'll give this a try and see if the `@` symbol is added again or it simply works as expected. Thanks for the answer! – Tim Lewis Apr 08 '23 at 20:46
  • I just found out about this: https://docs.laravel-excel.com/3.1/imports/multiple-sheets.html#making-calculations-work-when-referencing-between-sheets . Have you tried implementing the `Maatwebsite\Excel\Concerns\WithCalculatedFormulas` concern? perhaps that is what is causing your formula to have that weird escaped value. – IGP Apr 08 '23 at 20:49
  • Yeah, I saw that too, but somewhat disregarded it as it was for the `Import` side, not the `Export` side; maybe it is available for both though. I'll give that a try on another Sheet that is having the same problem; see which solution sticks – Tim Lewis Apr 08 '23 at 20:51
  • Alrighty; update: your code is missing `$worksheet = $event->sheet`, but once added, and `WithEvents`, `AfterSheet` and `RegistersEventListeners` added, this code generates an extra row with the formulae, but `D` and `E` column still have the `@` sign I'm trying a couple more things, but it might be a me issue (Excel suggests the `@` symbol is used for different versions of Excel or the XLS/XLSX filetype) – Tim Lewis Apr 08 '23 at 21:03
  • 1
    Forgot to edit that out. I meant to write `$event->sheet->setCellValue` on each line – IGP Apr 08 '23 at 21:06
  • Do they have the `@` sign even after removing `SheetTwo!` ? What about specifying the export format as `\Maatwebsite\Excel\Excel::XLSX`? Also, as a sanity check after using `setCellValue`, what does `$event->sheet->getCell('D'. $newRow)->getValue()` return? Can you log it? And last voodoo fix I can think of right now: What about not using string interpolation when setting the formlas? – IGP Apr 08 '23 at 21:09
  • Yup, ends up as `=SUM(IF(@D2:D50<>"",1/COUNTIF(D2:D50,@D2:D50),0))`. Looks like the default `$writerType` when doing `Excel::download()` is `Xlsx` – Tim Lewis Apr 08 '23 at 21:14
  • And I'll try those other things too; I'll see what the value is, and try without interpolation (I've done concatenation too, to see if it was something I was typing ) – Tim Lewis Apr 08 '23 at 21:15
  • `dd($workSheet->getCell('D' . $newRow))` returns a `PhpOffice\PhpSpreadsheet\Cell\Cell` instance, with `-value: =SUM(IF(D2:D50<>"",1/COUNTIF(D2:D50,D2:D50),0))` (as a string, would be the same as `->getValue()`). Notice too that in my question, only 2 of the instance of `SheetTwo!` have the preceeding `@` symbol... In the `IF()` and following the `,`, but not for `COUNTIF()`... So weird... – Tim Lewis Apr 08 '23 at 21:17
  • Honestly at that point you're stuck with following the values down `phpspreadsheet` 's methods to see where it's making the change, or check if there's anything about this weirdness in their issues. – IGP Apr 08 '23 at 21:19
  • 1
    Yup, seems like it. Appreciate the suggestions and extra set of eyes; glad to know it's not something I'm doing. Cheers! – Tim Lewis Apr 08 '23 at 21:21
  • It is pretty weird. This is a formula I have used before and it's never failed me. It has an `IF` inside too. `->setCellValue("K$row", "=IF(SUM(H$row:J$row)=0,\"\",SUM(H$row:J$row))"`. I have `excel` at 3.1.48 and `phpspreadsheet` at 1.28. – IGP Apr 08 '23 at 21:25
  • Same! I've used Laravel Excel and PHPOffice for years now and never had this issue... It might be an issue with a newer version of PHPOffice/Spreadsheet; I'm using a fresh install of these, and seeing some chatter about Formula changes [in the issues](https://github.com/PHPOffice/PhpSpreadsheet/issues/3495), so I might open up a similar issue there. You weren't able to recreate this, right? Are you on the same version (1.28.0)? Edit: hadn't refreshed; same versions. Super weird... – Tim Lewis Apr 08 '23 at 21:30
  • I don't seem to have problems with my formula. I tried yours and it just doesn't work (`#!VALUE`). Then again, I'm using a rather old version of excel. The formula even gets properly translated to my locale (one of the worst things about excel) with the correct escape characters and all. – IGP Apr 08 '23 at 22:05