1<?php
2
3namespace Box\Spout\Writer\Common\Manager;
4
5use Box\Spout\Common\Entity\Row;
6use Box\Spout\Common\Exception\IOException;
7use Box\Spout\Common\Manager\OptionsManagerInterface;
8use Box\Spout\Writer\Common\Creator\InternalEntityFactory;
9use Box\Spout\Writer\Common\Creator\ManagerFactoryInterface;
10use Box\Spout\Writer\Common\Entity\Options;
11use Box\Spout\Writer\Common\Entity\Sheet;
12use Box\Spout\Writer\Common\Entity\Workbook;
13use Box\Spout\Writer\Common\Entity\Worksheet;
14use Box\Spout\Writer\Common\Helper\FileSystemWithRootFolderHelperInterface;
15use Box\Spout\Writer\Common\Manager\Style\StyleManagerInterface;
16use Box\Spout\Writer\Common\Manager\Style\StyleMerger;
17use Box\Spout\Writer\Exception\SheetNotFoundException;
18use Box\Spout\Writer\Exception\WriterException;
19
20/**
21 * Class WorkbookManagerAbstract
22 * Abstract workbook manager, providing the generic interfaces to work with workbook.
23 */
24abstract class WorkbookManagerAbstract implements WorkbookManagerInterface
25{
26    /** @var Workbook The workbook to manage */
27    protected $workbook;
28
29    /** @var OptionsManagerInterface */
30    protected $optionsManager;
31
32    /** @var WorksheetManagerInterface */
33    protected $worksheetManager;
34
35    /** @var StyleManagerInterface Manages styles */
36    protected $styleManager;
37
38    /** @var StyleMerger Helper to merge styles */
39    protected $styleMerger;
40
41    /** @var FileSystemWithRootFolderHelperInterface Helper to perform file system operations */
42    protected $fileSystemHelper;
43
44    /** @var InternalEntityFactory Factory to create entities */
45    protected $entityFactory;
46
47    /** @var ManagerFactoryInterface $managerFactory Factory to create managers */
48    protected $managerFactory;
49
50    /** @var Worksheet The worksheet where data will be written to */
51    protected $currentWorksheet;
52
53    /**
54     * @param Workbook $workbook
55     * @param OptionsManagerInterface $optionsManager
56     * @param WorksheetManagerInterface $worksheetManager
57     * @param StyleManagerInterface $styleManager
58     * @param StyleMerger $styleMerger
59     * @param FileSystemWithRootFolderHelperInterface $fileSystemHelper
60     * @param InternalEntityFactory $entityFactory
61     * @param ManagerFactoryInterface $managerFactory
62     */
63    public function __construct(
64        Workbook $workbook,
65        OptionsManagerInterface $optionsManager,
66        WorksheetManagerInterface $worksheetManager,
67        StyleManagerInterface $styleManager,
68        StyleMerger $styleMerger,
69        FileSystemWithRootFolderHelperInterface $fileSystemHelper,
70        InternalEntityFactory $entityFactory,
71        ManagerFactoryInterface $managerFactory
72    ) {
73        $this->workbook = $workbook;
74        $this->optionsManager = $optionsManager;
75        $this->worksheetManager = $worksheetManager;
76        $this->styleManager = $styleManager;
77        $this->styleMerger = $styleMerger;
78        $this->fileSystemHelper = $fileSystemHelper;
79        $this->entityFactory = $entityFactory;
80        $this->managerFactory = $managerFactory;
81    }
82
83    /**
84     * @return int Maximum number of rows/columns a sheet can contain
85     */
86    abstract protected function getMaxRowsPerWorksheet();
87
88    /**
89     * @param Sheet $sheet
90     * @return string The file path where the data for the given sheet will be stored
91     */
92    abstract protected function getWorksheetFilePath(Sheet $sheet);
93
94    /**
95     * @return Workbook
96     */
97    public function getWorkbook()
98    {
99        return $this->workbook;
100    }
101
102    /**
103     * Creates a new sheet in the workbook and make it the current sheet.
104     * The writing will resume where it stopped (i.e. data won't be truncated).
105     *
106     * @throws IOException If unable to open the sheet for writing
107     * @return Worksheet The created sheet
108     */
109    public function addNewSheetAndMakeItCurrent()
110    {
111        $worksheet = $this->addNewSheet();
112        $this->setCurrentWorksheet($worksheet);
113
114        return $worksheet;
115    }
116
117    /**
118     * Creates a new sheet in the workbook. The current sheet remains unchanged.
119     *
120     * @throws \Box\Spout\Common\Exception\IOException If unable to open the sheet for writing
121     * @return Worksheet The created sheet
122     */
123    private function addNewSheet()
124    {
125        $worksheets = $this->getWorksheets();
126
127        $newSheetIndex = \count($worksheets);
128        $sheetManager = $this->managerFactory->createSheetManager();
129        $sheet = $this->entityFactory->createSheet($newSheetIndex, $this->workbook->getInternalId(), $sheetManager);
130
131        $worksheetFilePath = $this->getWorksheetFilePath($sheet);
132        $worksheet = $this->entityFactory->createWorksheet($worksheetFilePath, $sheet);
133
134        $this->worksheetManager->startSheet($worksheet);
135
136        $worksheets[] = $worksheet;
137        $this->workbook->setWorksheets($worksheets);
138
139        return $worksheet;
140    }
141
142    /**
143     * @return Worksheet[] All the workbook's sheets
144     */
145    public function getWorksheets()
146    {
147        return $this->workbook->getWorksheets();
148    }
149
150    /**
151     * Returns the current sheet
152     *
153     * @return Worksheet The current sheet
154     */
155    public function getCurrentWorksheet()
156    {
157        return $this->currentWorksheet;
158    }
159
160    /**
161     * Sets the given sheet as the current one. New data will be written to this sheet.
162     * The writing will resume where it stopped (i.e. data won't be truncated).
163     *
164     * @param Sheet $sheet The "external" sheet to set as current
165     * @throws SheetNotFoundException If the given sheet does not exist in the workbook
166     * @return void
167     */
168    public function setCurrentSheet(Sheet $sheet)
169    {
170        $worksheet = $this->getWorksheetFromExternalSheet($sheet);
171        if ($worksheet !== null) {
172            $this->currentWorksheet = $worksheet;
173        } else {
174            throw new SheetNotFoundException('The given sheet does not exist in the workbook.');
175        }
176    }
177
178    /**
179     * @param Worksheet $worksheet
180     * @return void
181     */
182    private function setCurrentWorksheet($worksheet)
183    {
184        $this->currentWorksheet = $worksheet;
185    }
186
187    /**
188     * Returns the worksheet associated to the given external sheet.
189     *
190     * @param Sheet $sheet
191     * @return Worksheet|null The worksheet associated to the given external sheet or null if not found.
192     */
193    private function getWorksheetFromExternalSheet($sheet)
194    {
195        $worksheetFound = null;
196
197        foreach ($this->getWorksheets() as $worksheet) {
198            if ($worksheet->getExternalSheet() === $sheet) {
199                $worksheetFound = $worksheet;
200                break;
201            }
202        }
203
204        return $worksheetFound;
205    }
206
207    /**
208     * Adds a row to the current sheet.
209     * If shouldCreateNewSheetsAutomatically option is set to true, it will handle pagination
210     * with the creation of new worksheets if one worksheet has reached its maximum capicity.
211     *
212     * @param Row $row The row to be added
213     * @throws IOException If trying to create a new sheet and unable to open the sheet for writing
214     * @throws WriterException If unable to write data
215     * @return void
216     */
217    public function addRowToCurrentWorksheet(Row $row)
218    {
219        $currentWorksheet = $this->getCurrentWorksheet();
220        $hasReachedMaxRows = $this->hasCurrentWorksheetReachedMaxRows();
221
222        // if we reached the maximum number of rows for the current sheet...
223        if ($hasReachedMaxRows) {
224            // ... continue writing in a new sheet if option set
225            if ($this->optionsManager->getOption(Options::SHOULD_CREATE_NEW_SHEETS_AUTOMATICALLY)) {
226                $currentWorksheet = $this->addNewSheetAndMakeItCurrent();
227
228                $this->addRowToWorksheet($currentWorksheet, $row);
229            } else {
230                // otherwise, do nothing as the data won't be written anyways
231            }
232        } else {
233            $this->addRowToWorksheet($currentWorksheet, $row);
234        }
235    }
236
237    /**
238     * @return bool Whether the current worksheet has reached the maximum number of rows per sheet.
239     */
240    private function hasCurrentWorksheetReachedMaxRows()
241    {
242        $currentWorksheet = $this->getCurrentWorksheet();
243
244        return ($currentWorksheet->getLastWrittenRowIndex() >= $this->getMaxRowsPerWorksheet());
245    }
246
247    /**
248     * Adds a row to the given sheet.
249     *
250     * @param Worksheet $worksheet Worksheet to write the row to
251     * @param Row $row The row to be added
252     * @throws WriterException If unable to write data
253     * @return void
254     */
255    private function addRowToWorksheet(Worksheet $worksheet, Row $row)
256    {
257        $this->applyDefaultRowStyle($row);
258        $this->worksheetManager->addRow($worksheet, $row);
259
260        // update max num columns for the worksheet
261        $currentMaxNumColumns = $worksheet->getMaxNumColumns();
262        $cellsCount = $row->getNumCells();
263        $worksheet->setMaxNumColumns(\max($currentMaxNumColumns, $cellsCount));
264    }
265
266    /**
267     * @param Row $row
268     */
269    private function applyDefaultRowStyle(Row $row)
270    {
271        $defaultRowStyle = $this->optionsManager->getOption(Options::DEFAULT_ROW_STYLE);
272
273        if ($defaultRowStyle !== null) {
274            $mergedStyle = $this->styleMerger->merge($row->getStyle(), $defaultRowStyle);
275            $row->setStyle($mergedStyle);
276        }
277    }
278
279    /**
280     * Closes the workbook and all its associated sheets.
281     * All the necessary files are written to disk and zipped together to create the final file.
282     * All the temporary files are then deleted.
283     *
284     * @param resource $finalFilePointer Pointer to the spreadsheet that will be created
285     * @return void
286     */
287    public function close($finalFilePointer)
288    {
289        $this->closeAllWorksheets();
290        $this->closeRemainingObjects();
291        $this->writeAllFilesToDiskAndZipThem($finalFilePointer);
292        $this->cleanupTempFolder();
293    }
294
295    /**
296     * Closes custom objects that are still opened
297     *
298     * @return void
299     */
300    protected function closeRemainingObjects()
301    {
302        // do nothing by default
303    }
304
305    /**
306     * Writes all the necessary files to disk and zip them together to create the final file.
307     *
308     * @param resource $finalFilePointer Pointer to the spreadsheet that will be created
309     * @return void
310     */
311    abstract protected function writeAllFilesToDiskAndZipThem($finalFilePointer);
312
313    /**
314     * Closes all workbook's associated sheets.
315     *
316     * @return void
317     */
318    private function closeAllWorksheets()
319    {
320        $worksheets = $this->getWorksheets();
321
322        foreach ($worksheets as $worksheet) {
323            $this->worksheetManager->close($worksheet);
324        }
325    }
326
327    /**
328     * Deletes the root folder created in the temp folder and all its contents.
329     *
330     * @return void
331     */
332    protected function cleanupTempFolder()
333    {
334        $rootFolder = $this->fileSystemHelper->getRootFolder();
335        $this->fileSystemHelper->deleteFolderRecursively($rootFolder);
336    }
337}
338