From dac59ea79e0dc5aa32df2f2f49ead31e9675c86c Mon Sep 17 00:00:00 2001 From: aswinkumar863 Date: Sun, 15 May 2022 13:30:54 +0530 Subject: [PATCH] Load Table with XLSX Reader --- phpstan-baseline.neon | 5 - src/PhpSpreadsheet/Reader/Xlsx.php | 29 +++-- .../Reader/Xlsx/TableReader.php | 105 ++++++++++++++++++ .../Reader/Xlsx/TableTest.php | 41 +++++++ tests/data/Reader/XLSX/tableTest.xlsx | Bin 0 -> 10339 bytes 5 files changed, 164 insertions(+), 16 deletions(-) create mode 100644 src/PhpSpreadsheet/Reader/Xlsx/TableReader.php create mode 100644 tests/PhpSpreadsheetTests/Reader/Xlsx/TableTest.php create mode 100644 tests/data/Reader/XLSX/tableTest.xlsx diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon index 74087f44..3989e2cf 100644 --- a/phpstan-baseline.neon +++ b/phpstan-baseline.neon @@ -2395,11 +2395,6 @@ parameters: count: 2 path: src/PhpSpreadsheet/Reader/Xlsx.php - - - message: "#^Comparison operation \"\\>\" between SimpleXMLElement\\|null and 0 results in an error\\.$#" - count: 1 - path: src/PhpSpreadsheet/Reader/Xlsx.php - - message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\Reader\\\\Xlsx\\:\\:boolean\\(\\) has no return type specified\\.$#" count: 1 diff --git a/src/PhpSpreadsheet/Reader/Xlsx.php b/src/PhpSpreadsheet/Reader/Xlsx.php index a6e7fe03..166d9576 100644 --- a/src/PhpSpreadsheet/Reader/Xlsx.php +++ b/src/PhpSpreadsheet/Reader/Xlsx.php @@ -19,6 +19,7 @@ use PhpOffice\PhpSpreadsheet\Reader\Xlsx\Properties as PropertyReader; use PhpOffice\PhpSpreadsheet\Reader\Xlsx\SheetViewOptions; use PhpOffice\PhpSpreadsheet\Reader\Xlsx\SheetViews; use PhpOffice\PhpSpreadsheet\Reader\Xlsx\Styles; +use PhpOffice\PhpSpreadsheet\Reader\Xlsx\TableReader; use PhpOffice\PhpSpreadsheet\Reader\Xlsx\Theme; use PhpOffice\PhpSpreadsheet\Reader\Xlsx\WorkbookView; use PhpOffice\PhpSpreadsheet\ReferenceHelper; @@ -874,7 +875,8 @@ class Xlsx extends BaseReader } if ($this->readDataOnly === false) { - $this->readAutoFilterTables($xmlSheet, $docSheet, $dir, $fileWorksheet, $zip); + $this->readAutoFilter($xmlSheet, $docSheet); + $this->readTables($xmlSheet, $docSheet, $dir, $fileWorksheet, $zip); } if ($xmlSheet && $xmlSheet->mergeCells && $xmlSheet->mergeCells->mergeCell && !$this->readDataOnly) { @@ -1983,23 +1985,28 @@ class Xlsx extends BaseReader } } - private function readAutoFilterTables( + private function readAutoFilter( + SimpleXMLElement $xmlSheet, + Worksheet $docSheet + ): void { + if ($xmlSheet && $xmlSheet->autoFilter) { + (new AutoFilter($docSheet, $xmlSheet))->load(); + } + } + + private function readTables( SimpleXMLElement $xmlSheet, Worksheet $docSheet, string $dir, string $fileWorksheet, ZipArchive $zip ): void { - if ($xmlSheet && $xmlSheet->autoFilter) { - // In older files, autofilter structure is defined in the worksheet file - (new AutoFilter($docSheet, $xmlSheet))->load(); - } elseif ($xmlSheet && $xmlSheet->tableParts && $xmlSheet->tableParts['count'] > 0) { - // But for Office365, MS decided to make it all just a bit more complicated - $this->readAutoFilterTablesInTablesFile($xmlSheet, $dir, $fileWorksheet, $zip, $docSheet); + if ($xmlSheet && $xmlSheet->tableParts && (int) $xmlSheet->tableParts['count'] > 0) { + $this->readTablesInTablesFile($xmlSheet, $dir, $fileWorksheet, $zip, $docSheet); } } - private function readAutoFilterTablesInTablesFile( + private function readTablesInTablesFile( SimpleXMLElement $xmlSheet, string $dir, string $fileWorksheet, @@ -2022,8 +2029,8 @@ class Xlsx extends BaseReader $relationshipFilePath = File::realpath($relationshipFilePath); if ($this->fileExistsInArchive($this->zip, $relationshipFilePath)) { - $autoFilter = $this->loadZip($relationshipFilePath); - (new AutoFilter($docSheet, $autoFilter))->load(); + $tableXml = $this->loadZip($relationshipFilePath); + (new TableReader($docSheet, $tableXml))->load(); } } } diff --git a/src/PhpSpreadsheet/Reader/Xlsx/TableReader.php b/src/PhpSpreadsheet/Reader/Xlsx/TableReader.php new file mode 100644 index 00000000..e2c06da0 --- /dev/null +++ b/src/PhpSpreadsheet/Reader/Xlsx/TableReader.php @@ -0,0 +1,105 @@ +worksheet = $workSheet; + $this->tableXml = $tableXml; + } + + /** + * Loads Table into the Worksheet. + */ + public function load(): void + { + // Remove all "$" in the table range + $tableRange = (string) preg_replace('/\$/', '', $this->tableXml['ref'] ?? ''); + if (strpos($tableRange, ':') !== false) { + $this->readTable($tableRange, $this->tableXml); + } + } + + /** + * Read Table from xml. + */ + private function readTable(string $tableRange, SimpleXMLElement $tableXml): void + { + $table = new Table($tableRange); + $table->setName((string) $tableXml['displayName']); + $table->setShowHeaderRow((string) $tableXml['headerRowCount'] !== '0'); + $table->setShowTotalsRow((string) $tableXml['totalsRowCount'] === '1'); + + $this->readTableAutoFilter($table, $tableXml->autoFilter); + $this->readTableColumns($table, $tableXml->tableColumns); + $this->readTableStyle($table, $tableXml->tableStyleInfo); + + $this->worksheet->addTable($table); + } + + /** + * Reads TableAutoFilter from xml. + */ + private function readTableAutoFilter(Table $table, SimpleXMLElement $autoFilterXml): void + { + foreach ($autoFilterXml->filterColumn as $filterColumn) { + $column = $table->getColumnByOffset((int) $filterColumn['colId']); + $column->setShowFilterButton((string) $filterColumn['hiddenButton'] !== '1'); + } + } + + /** + * Reads TableColumns from xml. + */ + private function readTableColumns(Table $table, SimpleXMLElement $tableColumnsXml): void + { + foreach ($tableColumnsXml->tableColumn as $tableColumn) { + $column = $table->getColumnByOffset((int) $tableColumn['id'] - 1); + + if ($table->getShowTotalsRow()) { + if ($tableColumn['totalsRowLabel']) { + $column->setTotalsRowLabel((string) $tableColumn['totalsRowLabel']); + } + + if ($tableColumn['totalsRowFunction']) { + $column->setTotalsRowFunction((string) $tableColumn['totalsRowFunction']); + } + } + + if ($tableColumn->calculatedColumnFormula) { + $column->setColumnFormula((string) $tableColumn->calculatedColumnFormula); + } + } + } + + /** + * Reads TableStyle from xml. + */ + private function readTableStyle(Table $table, SimpleXMLElement $tableStyleInfoXml): void + { + $tableStyle = new TableStyle(); + $tableStyle->setTheme((string) $tableStyleInfoXml['name']); + $tableStyle->setShowRowStripes((string) $tableStyleInfoXml['showRowStripes'] === '1'); + $tableStyle->setShowColumnStripes((string) $tableStyleInfoXml['showColumnStripes'] === '1'); + $tableStyle->setShowFirstColumn((string) $tableStyleInfoXml['showFirstColumn'] === '1'); + $tableStyle->setShowLastColumn((string) $tableStyleInfoXml['showLastColumn'] === '1'); + $table->setStyle($tableStyle); + } +} diff --git a/tests/PhpSpreadsheetTests/Reader/Xlsx/TableTest.php b/tests/PhpSpreadsheetTests/Reader/Xlsx/TableTest.php new file mode 100644 index 00000000..73e8d111 --- /dev/null +++ b/tests/PhpSpreadsheetTests/Reader/Xlsx/TableTest.php @@ -0,0 +1,41 @@ +load($filename); + + $worksheet = $spreadsheet->getActiveSheet(); + + $tables = $worksheet->getTableCollection(); + self::assertCount(1, $tables); + + $table = $tables->offsetGet(0); + self::assertInstanceOf(Table::class, $table); + self::assertEquals('SalesData', $table->getName()); + self::assertEquals('A1:G16', $table->getRange()); + self::assertTrue($table->getShowHeaderRow(), 'ShowHeaderRow'); + self::assertTrue($table->getShowTotalsRow(), 'ShowTotalsRow'); + + self::assertEquals('Total', $table->getColumn('B')->getTotalsRowLabel()); + self::assertEquals('sum', $table->getColumn('G')->getTotalsRowFunction()); + self::assertEquals('SUM(SalesData[[#This Row],[Q1]:[Q4]])', $table->getColumn('G')->getColumnFormula()); + + $tableStyle = $table->getStyle(); + self::assertEquals(TableStyle::TABLE_STYLE_MEDIUM4, $tableStyle->getTheme()); + self::assertTrue($tableStyle->getShowRowStripes(), 'ShowRowStripes'); + self::assertFalse($tableStyle->getShowColumnStripes(), 'ShowColumnStripes'); + self::assertFalse($tableStyle->getShowFirstColumn(), 'ShowFirstColumn'); + self::assertTrue($tableStyle->getShowLastColumn(), 'ShowLastColumn'); + } +} diff --git a/tests/data/Reader/XLSX/tableTest.xlsx b/tests/data/Reader/XLSX/tableTest.xlsx new file mode 100644 index 0000000000000000000000000000000000000000..c707c9e7ab5e3ae2038e10726748b06c4486cb83 GIT binary patch literal 10339 zcmeHt^EtS8FSZ_Xu!LvH)(yly54n;ts+rf8zuYrm^($+aFTR}wZuNGIK`6t5phaVk>s9KxL~JU-X<$F0-h8o+S=ohG1y4`JYS3;z-ji zX`hBc?2LPh*uTBX6I_e|cI%L&#_##u=#ltx-2`SGp&&DK0$3vl%P zXhL3MR{H$-xG$+EAp0X+aCM)`7t8Ej>2t&%@!ITC!Hidif;*B}>RKYhd$btHSrP#4s8spB~y1!83%}HkN>x%22P7%pkk&rcMQS z4y-{cUMZhDbRMneDuM?H2$U8)Y0u{>GD!M7%bM9Zf8>i)+a0eJ#M#1Z?d}d9p!he3 ztyW{DI)SykfcYaj%whE%AXcwgnE&|wpQHX4d*@%KUK0D$Q3Lm-Zi0HxCl;cyMP*%{ zNi|TadihAtV^v4wP(NH~c}$9}3VI7K>D}aY^LcJTAY!|h@@$2(Bn%gypQ^^CG%)GP z-U*qW)*(^CzT|5sj`PIn#94}@jN23ErYL%7U17Ft-x8I?*x~bX>>*|i5Gr0CX&|9s zim!IJoYtb@McIQ1QI*}&z>l?0v$o@g(!8e<^S3aBgLz+kA59_XeQjtuQ|8fYMR|G& zQd2SGHUDUs>A?5MP2bq6_4s*O3*M~fvXkEpMK(E>tJbUYis#OT>H1rJb(o^ zSl|EdrzE!DqLURUejf1S?MSmdyQgBHy~&7jvUQr5k6k>^3qxcb&$WF^Wxl@FXqyT) zsrGYlNW+}BOX&qQImnd2SQ^c*#2I2_iMZd@KR^}SY9?!yL4yd7IK45Y(V$@wvs&;f zRVcX7@GV@&ylozERxyauUT}t|D1ATLXv=aYN61jlAubB2~2op?P(1ZzB6{-5;mB#fZ%fZWc0yw!=!4z(6ffN$uRrhC2&RkGRMwe?m`ZwSM- z{&yP+ZAQN)hJh>?0RX^)^?=#XU#%xs<)!T`E57Hus=HU0O>0K+)i~1CBiWR4cote) zV!5Arhz!4em>Kl(Zb{1pa_aHz(lk#!6*?2#m_J<5lRiRECz9CUB14ZC@7;Bz!RigW>z&gqEh-uc%U2Y<*R}SU+^{ zil{&-$5r+hLzW3ynX$@=Fk%R?-jCa%Vh`+_E zb=&;$m)ZRn)wNPG)W=_CY2Z|O$qX!r{Vhns&NI!`CI40-k5I#t|rH1DjfO`>+*}Ds+zw0*&5-h?fmH;oCQjun9 zY4e4#d65iFe;(2*lwLKK->-BSimzsQ%3`k-^w~~-(DK@~Vi4cmuNc@H%ECaXdFfLI zry=C+?!i3t+%O6A0y%}))-5DD`NyH>%~R>FIx^qK5BR-fv*r4{=o}%=yj{M0>0J#r z?sUb98=MhAwVM0q&3@D4lm+CHQyIl0B#uuxUBzp07gF*FYWwX)=2B9t6Ggm@;ozS_ zW6OM3r3a~PZ+&IYSD!S=2`Ty=_ZiGe@wSP&H(Ouv%y#{Av~KY=GTea~&pZrZ@c%TT z*JcI|5Mvcb2Xhg9AY}tq!pMMe;?P1Ljz+ne;2k)CWAS6I=rsXp`CaV!AQzGfv7S zn?f3{d}8hl5wxb9X)Yh78Mw7?yG^g%6Xa=HsuLU&bTu4QJ}!Xs@swaM>4pIEcGZ$*lUSnMwExsDD4G9GqtJuw7>2{Ydst!mDWDOyC^WFe zq59=vugxG3$JhS??4KWO|AA}&s5Z2AR_y2des|bsHluH_WAdAVhoPIuC0jKZHPXEj zBP~yt#h)E;e#g8vDX;(^Q}5x=yeY_zM?`VxcA3Ng4Hk4*p!?y~V?N%O>WCvwee zlGEU`q`++JBNb9hVih$0yy@7q_{YKPC%ibCL98JxKYxGD_`BAC&BLeI%|x60*zO99GqFY_G4Qg9E8O;m z(=wr?MFiFT$ql;E@nj33YnoXiBUPJuBDR9~*6`FKEL&rBawGX7df8$QG%ZnVfb!m?Ju^m0_uxM2t@>kujN5i|H zOH6SudhgTgY0&kd_+5U|?HPEYR;;P79m@tZa!Vni4l)&EW-nQWN+lw}k^9bvGcjq< zLuEa`MmvaZ{LrW&PlLQ+dXnA;I>RVdZ%t676K8#*Ym`hEgnx&Vn@G>L=SnI>H*$Xyw zo@gWvHDvxD9(a^0x0^l*4m5Y%q6rkx2s32rIRCT>Cjw8R&M9f38%-HW89K+agC0ty zx|}XcT*#Vy1ZAISWa@W+`>z3_mb?Ye$xi?O?EYaN#*1Mad;|&9f zwJEDRmS&Hmxyr{Z%~vPq`(WhdG`RDZu2Ck>-|MWQ>>dKoOn|GF@>eS!9B=6I7lG-1 z-+eYCWdxT?*-R*P!!EiT7<{xvTX%Azh)+CZDH=9p#0U=e!*4>>k*3Nce2WwnrlLOF1Kp75i7&(>4ck6OdK{8s6gQrVl& zq}JuWddVn57J6X+CYfUfgpIu><6E+DZb_{LebPxOub-&eC!o1sZaL!tkAP> zVRm<-a7*nu9!VmDU*b0Te|YsQjd*9a5Pdu9syquX^zATpsxO+Y3=qGLX0Q3s9X=nQ zZhO#tcM=%m?@c<{7v5vOb8ZG#aea5dcs%79gS!x#n8!_nNCOgHI`-4I9P zPig6ll9B_3O8RAzPrAi9K%!|F98RL!|$u@7~J>GhD2ZxwR%}) z2F_~+LjX;}QOqMlVKUNep@$)Yz`dz#v}+PY)N{%}oD-tD*|ACPC=UnbxWOhx4j%qU zt(cWq8>dit_L+7$u!0loQb~=3;IxjSOXa6y+pgRjMC&e$)w>z>6E`>_d^%@GOJ}imRucV*i3eUz$nVXWv{s#qBX_4T3%B~a#f&^%I*jPD%wVj z3?gNNh%D`uJdQfk5Hmw;g5QhhyxX7Kix(aT`JZwkmu*)HiqiSl8^pyT0r!k=E|>a& z+mGveFALO#xKoC7z?<8xiJHzoNGxMg#jXaGhEXy-^Yc?j+bl89UW2O>%h#P7b%l?W zbXvLa=9w9SLy|+2XOd{HM0O7am(>yWg{<5HD2iET+TqRps!pbZ+WRnPUyZ)EYExFP zCDM~czqZ`aealQp5PKZm{II2}T=Y z{29GoJGxmxekL@z@jA9?toS}lk9UPNy$X;(71VIWd34HoWh}e7O(50~HT`I7z6$d@ z&z@*>wE}C4V{CHrxus8WdZ)@+rY4M2^c^GO!8j@jWuInEE_RG?7+EVKi&N1`H3QGT ztUzy8gU0KdUSaWflfwB|kSy{}m7$bRL`qw*IQHYGbIwX_zp||9tO*>My^>Up<^gUz zS1vn{((K#zrie`~D8ry<(%E+HLv*?J4TvdGuhq%JU_z{$mE_RQ7D`RKFf&kWVt?cY zshWsr06ZCy>tHyWoubjg)y|Oi@q9=pBSjqLyJ4P_P+2&d;SlYW&d@}`Y_<8c0d0qf_uT?*gNSx=UQCq36+j2_vVASpd_2L(Ua-UVv^ziOXtWbdf z4?Yvs2M-;i$oq^ut3Dp|42oC|?N?Yw}?K)zEZS=M=<$`sFftl4i{~Q!w($1Kt zHUi!<4`=qf-#5GUA+L1q63M^&LZ#maT)Mu*874+Xpyy{M)$q0>PImH^f40f6e8ULr z97B)N6u95-q2G@?a}BMr@B`+S+x%Rn1a>nKe%}e#ADhvA~U;BQ> zn48!B*2qm%oW8WqP5;z_2R(7VWNx?8rdGS$lHYY?ISb(SM%{?3NHVcC;}kDp1Fk-=?A`euY(JWp6jBovo9@NJ;x8~#{n!6AeG!e zR?un_CW27AsR@%k);o+4OVhWjf)m7ZWTxDk(1cO5>x05a&1sk8&&e_olsdKe{}3!g+3G6bn=cs<%5(iD-*dM~cf+TOCSi*t(}= zHFeBrq{MIZwWhYzvf4MxD&Hp-YOf?XgPS?wq=AQ%A1p=Dvr>sIx-Pt*s5VJY8EmN) z@yQhx2SjT_R{%QF{8&pVzSM!*pR{#eQFf%ZwpAAeH=kZt$tLR2K*z>QQW-5qwwIqX z?w(Yr;coAXCumQ_9@tL@aQP~%^smI5(q5A<>%F*(He^2c6_kWQ6Rj?dvfqQ5?9l=*2ZsBkIt>BlsbCc=5+Rk~<QtBOxQK(j>m?SdHK~4hUA`lsc~;^90*$9E(zUka_hNax z@uL7U@fmo%sWGa3;Rs6qOgbY@=o03zX$dLTGV`L6qVEtK%Q6wjobFQ?nmUM0@DP)5 zXbHmtzY<~AylZ!|=nW+4>u`)kYG%JT?9~4Iu{InMsMiZ?34)#YabV1=v8|D!gRR|b z79(2+$iJ$I|07&s(I`A_z-FETKj1v!3bS`-_Chj5xnC@oJG87_#A(oEWxGBsFytkT$q9sXFP#XI& zOyc7R)8$Q(VZHI^*2O0n-KZaX<8ql&7R4i8*LO}=An{q?eTZ&$Rn5;CWRZ*Df;`rS z9~?UYxhy{^h+IJ>-bocEeiXNDehZtjFduz-6`RMz$dEdk zZC8BYaDdptCYmh-FXb&n!ffLmD(d>>jKER39(HpUh?;oYCui|0$^Q(?y;IwQX_|U^ zW`$O^c^!NU zXG8o-CsHyXR(lsTqn(Irg+1Iked_>LeJc@Iv@@I^2R)YPt~%wy%8=9wKw<+8m}=Fm z&?1UxJy}O2Jtp|bEe#lpN@%$-SQebYtDstoXVEvmg{m}D7|ZvDN3R`Mz1hf(73v#$ zDpl&asOkOIYmzcy6IbVv>`Upl9v1~inE$i{Q<&Tp)v)QmgCW4bC{ITNL)g)P<>%`s zvlXuL(sG^^+k@pq808d}DL2a=D`C!M%h{t+2kg8Af40YZXvsp@8yhnzx3}cNw8Sef zwG4L2O<5Qz84c^14ffuA>&zWDmJ@<+CRS(2PrTByv z>iW*_b?L}J@N-Kzx;||Ocqq9Cb$x5+Ti>TnUZ6Q3(B9Z%C6!f=7p>7%t)W??+-~mC zky|}--Fl&;qr~^R%UUaER3`g&1Xui;y>Dzynri>$SxIZv^hWx(_tzP-9LU}{9tB57 z?T3qoq?5LitNI;eptHppw}>kzg^c5VV1C$SBEA^9^(|=EILm&&!@K}K;u83ZMS`q6aCVarM>55=ZPtyqzKFrK6zEfWqXC zg831lpq#`r`?DPS@1nZHlgkMUb%&7>2S^DvmS+#?&lSzc3*Rxt-GuRd2W3(Wmc4bY zLUdc8xJxA)k3(|AO=882y+i(|ZJr1-K97KH^JCaHWB(QC4eadxhx0Jk|JRcq+h#Mz z3JlzbKf?^%i%2hf5-6gsl*{x+a|9sW!0~vjTv3aidQ$4_B+q)>X*R=|>JqO-C`MHy zs2f7oZTyy{(;+4d*o2yQdMzrjyEo_+%7}A;t9zgsOhs#-*W#^`Mywrf%m%UgszPb; z-kAo%&cz)~$mp(KjQx;8epN59xB|)6&&_%`PGIO*HUwEz-_UJRKG4un*Xg7sAHpiA z|7MX)2WfJ<-a3OzxHGTmM<3Ma)B*>k$pj zCj&;zuZ=F?IrJt{5D$;_cMJ(^LU|mQMAG~o3;38H&l=P5uz*zikGoldJ3d^NE1Y31 z8$`tf&}j$dBYLlm;@?*Mpb@NYu!HyC7ZYMCw#LAbUF@&&FUs0;YaThJ-2vTz`_D5! zh{qyaoe{b2p-~VtIN>TRj3jTf@ z-~cfIN-&GRq5k9R{{VA~()|Dc literal 0 HcmV?d00001