1<?php
2/**
3 * Zend Framework (http://framework.zend.com/)
4 *
5 * @link      http://github.com/zendframework/zf2 for the canonical source repository
6 * @copyright Copyright (c) 2005-2015 Zend Technologies USA Inc. (http://www.zend.com)
7 * @license   http://framework.zend.com/license/new-bsd New BSD License
8 */
9
10namespace Zend\Config\Writer;
11
12use Zend\Config\Exception;
13
14class Ini extends AbstractWriter
15{
16    /**
17     * Separator for nesting levels of configuration data identifiers.
18     *
19     * @var string
20     */
21    protected $nestSeparator = '.';
22
23    /**
24     * If true the INI string is rendered in the global namespace without
25     * sections.
26     *
27     * @var bool
28     */
29    protected $renderWithoutSections = false;
30
31    /**
32     * Set nest separator.
33     *
34     * @param  string $separator
35     * @return self
36     */
37    public function setNestSeparator($separator)
38    {
39        $this->nestSeparator = $separator;
40        return $this;
41    }
42
43    /**
44     * Get nest separator.
45     *
46     * @return string
47     */
48    public function getNestSeparator()
49    {
50        return $this->nestSeparator;
51    }
52
53    /**
54     * Set if rendering should occur without sections or not.
55     *
56     * If set to true, the INI file is rendered without sections completely
57     * into the global namespace of the INI file.
58     *
59     * @param  bool $withoutSections
60     * @return Ini
61     */
62    public function setRenderWithoutSectionsFlags($withoutSections)
63    {
64        $this->renderWithoutSections = (bool) $withoutSections;
65        return $this;
66    }
67
68    /**
69     * Return whether the writer should render without sections.
70     *
71     * @return bool
72     */
73    public function shouldRenderWithoutSections()
74    {
75        return $this->renderWithoutSections;
76    }
77
78    /**
79     * processConfig(): defined by AbstractWriter.
80     *
81     * @param  array $config
82     * @return string
83     */
84    public function processConfig(array $config)
85    {
86        $iniString = '';
87
88        if ($this->shouldRenderWithoutSections()) {
89            $iniString .= $this->addBranch($config);
90        } else {
91            $config = $this->sortRootElements($config);
92
93            foreach ($config as $sectionName => $data) {
94                if (!is_array($data)) {
95                    $iniString .= $sectionName
96                               .  ' = '
97                               .  $this->prepareValue($data)
98                               .  "\n";
99                } else {
100                    $iniString .= '[' . $sectionName . ']' . "\n"
101                               .  $this->addBranch($data)
102                               .  "\n";
103                }
104            }
105        }
106
107        return $iniString;
108    }
109
110    /**
111     * Add a branch to an INI string recursively.
112     *
113     * @param  array $config
114     * @param  array $parents
115     * @return string
116     */
117    protected function addBranch(array $config, $parents = array())
118    {
119        $iniString = '';
120
121        foreach ($config as $key => $value) {
122            $group = array_merge($parents, array($key));
123
124            if (is_array($value)) {
125                $iniString .= $this->addBranch($value, $group);
126            } else {
127                $iniString .= implode($this->nestSeparator, $group)
128                           .  ' = '
129                           .  $this->prepareValue($value)
130                           .  "\n";
131            }
132        }
133
134        return $iniString;
135    }
136
137    /**
138     * Prepare a value for INI.
139     *
140     * @param  mixed $value
141     * @return string
142     * @throws Exception\RuntimeException
143     */
144    protected function prepareValue($value)
145    {
146        if (is_int($value) || is_float($value)) {
147            return $value;
148        } elseif (is_bool($value)) {
149            return ($value ? 'true' : 'false');
150        } elseif (false === strpos($value, '"')) {
151            return '"' . $value .  '"';
152        } else {
153            throw new Exception\RuntimeException('Value can not contain double quotes');
154        }
155    }
156
157    /**
158     * Root elements that are not assigned to any section needs to be on the
159     * top of config.
160     *
161     * @param  array $config
162     * @return array
163     */
164    protected function sortRootElements(array $config)
165    {
166        $sections = array();
167
168        // Remove sections from config array.
169        foreach ($config as $key => $value) {
170            if (is_array($value)) {
171                $sections[$key] = $value;
172                unset($config[$key]);
173            }
174        }
175
176        // Read sections to the end.
177        foreach ($sections as $key => $value) {
178            $config[$key] = $value;
179        }
180
181        return $config;
182    }
183}
184