1<?php
2
3namespace Box\Spout\Writer\Common\Manager;
4
5use Box\Spout\Common\Helper\StringHelper;
6use Box\Spout\Writer\Common\Entity\Sheet;
7use Box\Spout\Writer\Exception\InvalidSheetNameException;
8
9/**
10 * Class SheetManager
11 * Sheet manager
12 */
13class SheetManager
14{
15    /** Sheet name should not exceed 31 characters */
16    const MAX_LENGTH_SHEET_NAME = 31;
17
18    /** @var array Invalid characters that cannot be contained in the sheet name */
19    private static $INVALID_CHARACTERS_IN_SHEET_NAME = ['\\', '/', '?', '*', ':', '[', ']'];
20
21    /** @var array Associative array [WORKBOOK_ID] => [[SHEET_INDEX] => [SHEET_NAME]] keeping track of sheets' name to enforce uniqueness per workbook */
22    private static $SHEETS_NAME_USED = [];
23
24    /** @var StringHelper */
25    private $stringHelper;
26
27    /**
28     * SheetManager constructor.
29     *
30     * @param StringHelper $stringHelper
31     */
32    public function __construct(StringHelper $stringHelper)
33    {
34        $this->stringHelper = $stringHelper;
35    }
36
37    /**
38     * Throws an exception if the given sheet's name is not valid.
39     * @see Sheet::setName for validity rules.
40     *
41     * @param string $name
42     * @param Sheet $sheet The sheet whose future name is checked
43     * @throws \Box\Spout\Writer\Exception\InvalidSheetNameException If the sheet's name is invalid.
44     * @return void
45     */
46    public function throwIfNameIsInvalid($name, Sheet $sheet)
47    {
48        if (!is_string($name)) {
49            $actualType = gettype($name);
50            $errorMessage = "The sheet's name is invalid. It must be a string ($actualType given).";
51            throw new InvalidSheetNameException($errorMessage);
52        }
53
54        $failedRequirements = [];
55        $nameLength = $this->stringHelper->getStringLength($name);
56
57        if (!$this->isNameUnique($name, $sheet)) {
58            $failedRequirements[] = 'It should be unique';
59        } else {
60            if ($nameLength === 0) {
61                $failedRequirements[] = 'It should not be blank';
62            } else {
63                if ($nameLength > self::MAX_LENGTH_SHEET_NAME) {
64                    $failedRequirements[] = 'It should not exceed 31 characters';
65                }
66
67                if ($this->doesContainInvalidCharacters($name)) {
68                    $failedRequirements[] = 'It should not contain these characters: \\ / ? * : [ or ]';
69                }
70
71                if ($this->doesStartOrEndWithSingleQuote($name)) {
72                    $failedRequirements[] = 'It should not start or end with a single quote';
73                }
74            }
75        }
76
77        if (count($failedRequirements) !== 0) {
78            $errorMessage = "The sheet's name (\"$name\") is invalid. It did not respect these rules:\n - ";
79            $errorMessage .= implode("\n - ", $failedRequirements);
80            throw new InvalidSheetNameException($errorMessage);
81        }
82    }
83
84    /**
85     * Returns whether the given name contains at least one invalid character.
86     * @see Sheet::$INVALID_CHARACTERS_IN_SHEET_NAME for the full list.
87     *
88     * @param string $name
89     * @return bool TRUE if the name contains invalid characters, FALSE otherwise.
90     */
91    private function doesContainInvalidCharacters($name)
92    {
93        return (str_replace(self::$INVALID_CHARACTERS_IN_SHEET_NAME, '', $name) !== $name);
94    }
95
96    /**
97     * Returns whether the given name starts or ends with a single quote
98     *
99     * @param string $name
100     * @return bool TRUE if the name starts or ends with a single quote, FALSE otherwise.
101     */
102    private function doesStartOrEndWithSingleQuote($name)
103    {
104        $startsWithSingleQuote = ($this->stringHelper->getCharFirstOccurrencePosition('\'', $name) === 0);
105        $endsWithSingleQuote = ($this->stringHelper->getCharLastOccurrencePosition('\'', $name) === ($this->stringHelper->getStringLength($name) - 1));
106
107        return ($startsWithSingleQuote || $endsWithSingleQuote);
108    }
109
110    /**
111     * Returns whether the given name is unique.
112     *
113     * @param string $name
114     * @param Sheet $sheet The sheet whose future name is checked
115     * @return bool TRUE if the name is unique, FALSE otherwise.
116     */
117    private function isNameUnique($name, Sheet $sheet)
118    {
119        foreach (self::$SHEETS_NAME_USED[$sheet->getAssociatedWorkbookId()] as $sheetIndex => $sheetName) {
120            if ($sheetIndex !== $sheet->getIndex() && $sheetName === $name) {
121                return false;
122            }
123        }
124
125        return true;
126    }
127
128    /**
129     * @param int $workbookId Workbook ID associated to a Sheet
130     * @return void
131     */
132    public function markWorkbookIdAsUsed($workbookId)
133    {
134        if (!isset(self::$SHEETS_NAME_USED[$workbookId])) {
135            self::$SHEETS_NAME_USED[$workbookId] = [];
136        }
137    }
138
139    /**
140     * @param Sheet $sheet
141     * @return void
142     */
143    public function markSheetNameAsUsed(Sheet $sheet)
144    {
145        self::$SHEETS_NAME_USED[$sheet->getAssociatedWorkbookId()][$sheet->getIndex()] = $sheet->getName();
146    }
147}
148