diff --git a/docs/languages/en/conf.py b/docs/languages/en/conf.py index 5f141ab56..ad3cce6f2 100644 --- a/docs/languages/en/conf.py +++ b/docs/languages/en/conf.py @@ -50,7 +50,7 @@ # The short X.Y version. version = '2.3' # The full version, including alpha/beta/rc tags. -release = '2.3.3dev' +release = '2.3.4dev' # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. diff --git a/docs/languages/en/in-depth-guide/data-binding.rst b/docs/languages/en/in-depth-guide/data-binding.rst new file mode 100644 index 000000000..9c1c4c61b --- /dev/null +++ b/docs/languages/en/in-depth-guide/data-binding.rst @@ -0,0 +1,750 @@ +Editting and Deleting Data +========================== + +In the previous chapter we've come to learn how we can use the ``Zend\Form``- and ``Zend\Db``-components to create the +functionality of creating new data-sets. This chapter will focus on finalizing the CRUD functionality by introducing +the concepts for editting and deleting data. We start by editting the data. + + +Binding Objects to Forms +======================== + +The one fundamental difference between an insert- and an edit-form is the fact that inside an edit-form there is +already data preset. This means we need to find a way to get data from our database into the form. Luckily ``Zend\Form`` +provides us with a very handy way of doing so and it's called **data-binding**. + +All you need to do when providing an edit-form is to get the object of interest from your service and ``bind`` it to the +form. This is done the following way inside your controller. + +.. code-block:: php + :linenos: + :emphasize-lines: 50-53 + + postService = $postService; + $this->postForm = $postForm; + } + + public function addAction() + { + $request = $this->getRequest(); + + if ($request->isPost()) { + $this->postForm->setData($request->getPost()); + + if ($this->postForm->isValid()) { + try { + $this->postService->savePost($this->postForm->getData()); + + return $this->redirect()->toRoute('post'); + } catch (\Exception $e) { + die($e->getMessage()); + // Some DB Error happened, log it and let the user know + } + } + } + + return new ViewModel(array( + 'form' => $this->postForm + )); + } + + public function editAction() + { + $request = $this->getRequest(); + $post = $this->postService->findPost($this->params('id')); + + $this->postForm->bind($post); + + if ($request->isPost()) { + $this->postForm->setData($request->getPost()); + + if ($this->postForm->isValid()) { + try { + $this->postService->savePost($post); + + return $this->redirect()->toRoute('post'); + } catch (\Exception $e) { + die($e->getMessage()); + // Some DB Error happened, log it and let the user know + } + } + } + + return new ViewModel(array( + 'form' => $this->postForm + )); + } + } + +Compared to the ``addAction()`` the ``editAction()`` has only three different lines. The first one is used to simply +get the relevant ``Post``-object from the service identified by the ``id``-parameter of the route (which we'll be +writing soon). + +The second line then shows you how you can bind data to the ``Zend\Form``-Component. We're able to use an object here +because our ``PostFieldset`` will use the hydrator to display the data coming from the object. + +Lastly instead of actually doing ``$form->getData()`` we simply use the previous ``$post``-variable since it will be +updated with the latest data from the form thanks to the data-binding. And that's all there is to it. The only things +we need to add now is the new edit-route and the view for it. + + +Adding the edit-route +===================== + +The edit route is a normal segment route just like the route ``blog/detail``. Configure your route config to include the +new route: + +.. code-block:: php + :linenos: + :emphasize-lines: 43-55 + + array( /** Db Config */ ), + 'service_manager' => array( /** ServiceManager Config */ ), + 'view_manager' => array( /** ViewManager Config */ ), + 'controllers' => array( /** ControllerManager Config* */ ), + 'router' => array( + 'routes' => array( + 'blog' => array( + 'type' => 'literal', + 'options' => array( + 'route' => '/blog', + 'defaults' => array( + 'controller' => 'Blog\Controller\List', + 'action' => 'index', + ) + ), + 'may_terminate' => true, + 'child_routes' => array( + 'detail' => array( + 'type' => 'segment', + 'options' => array( + 'route' => '/:id', + 'defaults' => array( + 'action' => 'detail' + ), + 'constraints' => array( + 'id' => '\d+' + ) + ) + ), + 'add' => array( + 'type' => 'literal', + 'options' => array( + 'route' => '/add', + 'defaults' => array( + 'controller' => 'Blog\Controller\Write', + 'action' => 'add' + ) + ) + ), + 'edit' => array( + 'type' => 'segment', + 'options' => array( + 'route' => '/edit/:id', + 'defaults' => array( + 'controller' => 'Blog\Controller\Write', + 'action' => 'edit' + ), + 'constraints' => array( + 'id' => '\d+' + ) + ) + ), + ) + ) + ) + ) + ); + +Creating the edit-template +========================== + +Next in line is the creation of the new template ``blog/write/edit``: + +.. code-block:: php + :linenos: + :emphasize-lines: 6 + + +

WriteController::editAction()

+ form; + $form->setAttribute('action', $this->url('blog/edit', array(), true)); + $form->prepare(); + + echo $this->form()->openTag($form); + + echo $this->formCollection($form); + + echo $this->form()->closeTag(); + +All that is really changing on the view-end is that you need to pass the current ``id`` to the ``url()`` view helper. To +achieve this you have two options. The first one would be to pass the ID to the parameters array like + +.. code-block:: php + :linenos: + + $this->url('blog/edit', array('id' => $id)); + +The downside is that ``$id`` is not available as we have not assigned it to the view. The ``Zend\Mvc\Router``-component +however provides us with a nice functionality to re-use the currently matched parameters. This is done by setting the +last parameter of the view-helper to ``true``. + +.. code-block:: php + :linenos: + + $this->url('blog/edit', array(), true); + + +**Checking the status** + +If you go to your browser and open up the edit form at ``localhost:8080/blog/edit/1`` you'll see that the form contains +the data from your selected blog. And when you submit the form you'll notice that the data has been changed +successfully. However sadly the submit-button still contains the text ``Insert new Post``. This can be changed inside +the view, too. + +.. code-block:: php + :linenos: + :emphasize-lines: 9 + + +

WriteController::editAction()

+ form; + $form->setAttribute('action', $this->url('blog/edit', array(), true)); + $form->prepare(); + + $form->get('submit')->setValue('Update Post'); + + echo $this->form()->openTag($form); + + echo $this->formCollection($form); + + echo $this->form()->closeTag(); + + +Implementing the delete functionality +===================================== + +Last but not least it's time to delete some data. We start this process by creating a new route and adding a new +controller: + +.. code-block:: php + :linenos: + :emphasize-lines: 11, 62-74 + + array( /** Db Config */ ), + 'service_manager' => array( /** ServiceManager Config */ ), + 'view_manager' => array( /** ViewManager Config */ ), + 'controllers' => array( + 'factories' => array( + 'Blog\Controller\List' => 'Blog\Factory\ListControllerFactory', + 'Blog\Controller\Write' => 'Blog\Factory\WriteControllerFactory', + 'Blog\Controller\Delete' => 'Blog\Factory\DeleteControllerFactory' + ) + ), + 'router' => array( + 'routes' => array( + 'post' => array( + 'type' => 'literal', + 'options' => array( + 'route' => '/blog', + 'defaults' => array( + 'controller' => 'Blog\Controller\List', + 'action' => 'index', + ) + ), + 'may_terminate' => true, + 'child_routes' => array( + 'detail' => array( + 'type' => 'segment', + 'options' => array( + 'route' => '/:id', + 'defaults' => array( + 'action' => 'detail' + ), + 'constraints' => array( + 'id' => '\d+' + ) + ) + ), + 'add' => array( + 'type' => 'literal', + 'options' => array( + 'route' => '/add', + 'defaults' => array( + 'controller' => 'Blog\Controller\Write', + 'action' => 'add' + ) + ) + ), + 'edit' => array( + 'type' => 'segment', + 'options' => array( + 'route' => '/edit/:id', + 'defaults' => array( + 'controller' => 'Blog\Controller\Write', + 'action' => 'edit' + ), + 'constraints' => array( + 'id' => '\d+' + ) + ) + ), + 'delete' => array( + 'type' => 'segment', + 'options' => array( + 'route' => '/delete/:id', + 'defaults' => array( + 'controller' => 'Blog\Controller\Delete', + 'action' => 'delete' + ), + 'constraints' => array( + 'id' => '\d+' + ) + ) + ), + ) + ) + ) + ) + ); + +Notice here that we have assigned yet another controller ``Blog\Controller\Delete``. This is due to the fact that this +controller will **not** require the ``PostForm``. A ``DeleteForm`` is a perfect example for when you do not even need to +make use of the ``Zend\Form`` component. Let's go ahead and create our controller first: + +**The Factory** + +.. code-block:: php + :linenos: + + getServiceLocator(); + $postService = $realServiceLocator->get('Blog\Service\PostServiceInterface'); + + return new DeleteController($postService); + } + } + +**The Controller** + +.. code-block:: php + :linenos: + :emphasize-lines: 31-35 + + postService = $postService; + } + + public function deleteAction() + { + try { + $post = $this->postService->findPost($this->params('id')); + } catch (\InvalidArgumentException $e) { + return $this->redirect()->toRoute('post'); + } + + $request = $this->getRequest(); + + if ($request->isPost()) { + $del = $request->getPost('delete_confirmation', 'no'); + + if ($del === 'yes') { + $this->postService->deletePost($post); + } + + return $this->redirect()->toRoute('post'); + } + + return new ViewModel(array( + 'post' => $post + )); + } + } + +As you can see this is nothing new. We inject the ``PostService`` into the controller and inside the action we first +check if the blog exists. If so we check if it's a post request and inside there we check if a certain post parameter +called ``delete_confirmation`` is present. If the value of that then is ``yes`` we delete the blog through the +``PostService``'s ``deletePost()`` function. + +When you're writing this code you'll notice that you don't get typehints for the ``deletePost()`` function because we +haven't added it to the service / interface yet. Go ahead and add the function to the interface and implement it inside +the service. + +**The Interface** + +.. code-block:: php + :linenos: + :emphasize-lines: 41 + + postMapper = $postMapper; + } + + /** + * {@inheritDoc} + */ + public function findAllPosts() + { + return $this->postMapper->findAll(); + } + + /** + * {@inheritDoc} + */ + public function findPost($id) + { + return $this->postMapper->find($id); + } + + /** + * {@inheritDoc} + */ + public function savePost(PostInterface $post) + { + return $this->postMapper->save($post); + } + + /** + * {@inheritDoc} + */ + public function deletePost(PostInterface $post) + { + return $this->postMapper->delete($post); + } + } + +Now we assume that the ``PostMapperInterface`` has a ``delete()``-function. We haven't yet implemented this one so go +ahead and add it to the ``PostMapperInterface``. + +.. code-block:: php + :linenos: + :emphasize-lines: 36 + + dbAdapter = $dbAdapter; + $this->hydrator = $hydrator; + $this->postPrototype = $postPrototype; + } + + /** + * {@inheritDoc} + */ + public function find($id) + { + $sql = new Sql($this->dbAdapter); + $select = $sql->select('posts'); + $select->where(array('id = ?' => $id)); + + $stmt = $sql->prepareStatementForSqlObject($select); + $result = $stmt->execute(); + + if ($result instanceof ResultInterface && $result->isQueryResult() && $result->getAffectedRows()) { + return $this->hydrator->hydrate($result->current(), $this->postPrototype); + } + + throw new \InvalidArgumentException("Blog with given ID:{$id} not found."); + } + + /** + * {@inheritDoc} + */ + public function findAll() + { + $sql = new Sql($this->dbAdapter); + $select = $sql->select('posts'); + + $stmt = $sql->prepareStatementForSqlObject($select); + $result = $stmt->execute(); + + if ($result instanceof ResultInterface && $result->isQueryResult()) { + $resultSet = new HydratingResultSet($this->hydrator, $this->postPrototype); + + return $resultSet->initialize($result); + } + + return array(); + } + + /** + * {@inheritDoc} + */ + public function save(PostInterface $postObject) + { + $postData = $this->hydrator->extract($postObject); + unset($postData['id']); // Neither Insert nor Update needs the ID in the array + + if ($postObject->getId()) { + // ID present, it's an Update + $action = new Update('post'); + $action->set($postData); + $action->where(array('id = ?' => $postObject->getId())); + } else { + // ID NOT present, it's an Insert + $action = new Insert('post'); + $action->values($postData); + } + + $sql = new Sql($this->dbAdapter); + $stmt = $sql->prepareStatementForSqlObject($action); + $result = $stmt->execute(); + + if ($result instanceof ResultInterface) { + if ($newId = $result->getGeneratedValue()) { + // When a value has been generated, set it on the object + $postObject->setId($newId); + } + + return $postObject; + } + + throw new \Exception("Database error"); + } + + /** + * {@inheritDoc} + */ + public function delete(PostInterface $postObject) + { + $action = new Delete('post'); + $action->where(array('id = ?' => $postObject->getId())); + + $sql = new Sql($this->dbAdapter); + $stmt = $sql->prepareStatementForSqlObject($action); + $result = $stmt->execute(); + + return (bool)$result->getAffectedRows(); + } + } + +The ``Delete`` statement should look fairly similar to you as this is basically the same deal as all other queries we've +created so far. With all of this set up now we're good to go ahead and write our view file so we can delete blogs. + +.. code-block:: php + :linenos: + + +

DeleteController::deleteAction()

+

+ Are you sure that you want to delete + 'escapeHtml($this->blog->getTitle()); ?>' by + 'escapeHtml($this->blog->getText()); ?>'? +

+
+ + +
+ +Summary +======= + +In this chapter we've learned how data binding within the ``Zend\Form``-component works and through it we have finished +our update-routine. Then we have learned how we can use HTML-Forms and checking it's data without relying on +``Zend\Form``, which ultimately lead us to having a full CRUD-Routine for the Blog example. + +In the next chapter we'll recapitulate everything we've done. We'll talk about the design-patterns we've used and we're +going to cover a couple of questions that highly likely arose during the course of this tutorial. diff --git a/docs/languages/en/in-depth-guide/first-module.rst b/docs/languages/en/in-depth-guide/first-module.rst new file mode 100644 index 000000000..f96f0833a --- /dev/null +++ b/docs/languages/en/in-depth-guide/first-module.rst @@ -0,0 +1,370 @@ +.. _in-depth-guide.first-module: + +Introducing our first "Blog" Module +=================================== + +Now that we know about the basics of the Zend Framework 2 Skeleton Application, let's continue and create our very own +module. We will create a module named "Blog". This module will display a list of database entries that represent a +single blog post. Each post will have three properties: ``id``, ``text`` and ``title``. We will create +forms to enter new posts into our database and to edit existing posts. Furthermore we will do so by using +best-practices throughout the whole QuickStart. + + +Writing a new Module +==================== + +Let's start by creating a new folder under the ``/module`` directory called ``Blog``. + +To be recognized as a module by the :ref:`ModuleManager ` +all we need to do is create a PHP class named ``Module`` under our module's namespace, which is ``Blog``. Create the +file ``/module/Blog/Module.php`` + +.. code-block:: php + :linenos: + + `. +Let's add this module to our application. Although our module doesn't do anything yet, just having the ``Module.php`` +class allows it to be loaded by ZF2s :ref:`ModuleManager `. +To do this, add an entry for ``Blog`` to the modules array inside the main application config file at +``/config/application.config.php``: + +.. code-block:: php + :linenos: + :emphasize-lines: 6 + + array( + 'Application', + 'Blog' + ), + + // ... + ); + +If you refresh your application you should see no change at all (but also no errors). + +At this point it's worth taking a step back to discuss what modules are for. In short, a module is an encapsulated +set of features for your application. A module might add features to the application that you can see, like our +Blog module; or it might provide background functionality for other modules in the application to use, such as +interacting with a third party API. + +Organizing your code into modules makes it easier for you to reuse functionality in other application, or to use +modules written by the community. + +Configuring the Module +====================== + +The next thing we're going to do is add a route to our application so that our module can be accessed through the +URL ``localhost:8080/blog``. We do this by adding router configuration to our module, but first we need to let the +``ModuleManager`` know that our module has configuration that it needs to load. + +This is done by adding a ``getConfig()`` function to the ``Module`` class that returns the configuration. (This function is +defined in the ``ConfigProviderInterface`` although actually implementing this interface in the module class is optional.) +This function should return either an ``array`` or a ``Traversable`` object. Continue by editing your +``/module/Blog/Module.php``: + +.. code-block:: php + :linenos: + :emphasize-lines: 5,7,9-12 + + array( + // Open configuration for all possible routes + 'routes' => array( + // Define a new route called "post" + 'post' => array( + // Define the routes type to be "Zend\Mvc\Router\Http\Literal", which is basically just a string + 'type' => 'literal', + // Configure the route itself + 'options' => array( + // Listen to "/blog" as uri + 'route' => '/blog', + // Define default controller and action to be called when this route is matched + 'defaults' => array( + 'controller' => 'Blog\Controller\List', + 'action' => 'index', + ) + ) + ) + ) + ) + ); + +We've now created a route called ``blog`` that listens to the URL ``localhost:8080/blog``. Whenever someone accesses this +route, the ``indexAction()`` function of the class ``Blog\Controller\List`` will be executed. However, this controller +does not exist yet, so if you reload the page you will see this error message: + +.. code-block:: html + :linenos: + + A 404 error occurred + Page not found. + The requested controller could not be mapped to an existing controller class. + + Controller: + Blog\Controller\List(resolves to invalid controller class or alias: Blog\Controller\List) + No Exception available + +We now need to tell our module where to find this controller named ``Blog\Controller\List``. To achieve this we have +to add this key to the ``controllers`` configuration key inside your ``/module/Blog/config/module.config.php``. + +.. code-block:: php + :linenos: + :emphasize-lines: 4-8 + + array( + 'invokables' => array( + 'Blog\Controller\List' => 'Blog\Controller\ListController' + ) + ), + 'router' => array( /** Route Configuration */ ) + ); + +This configuration defines ``Blog\Controller\List`` as an alias for the ``ListController`` under the namespace +``Blog\Controller``. Reloading the page should then give you: + +.. code-block:: html + :linenos: + + ( ! ) Fatal error: Class 'Blog\Controller\ListController' not found in {libPath}/Zend/ServiceManager/AbstractPluginManager.php on line {lineNumber} + +This error tells us that the application knows what class to load, but not where to find it. To fix this, we need to +configure `autoloading `_ for our Module. Autoloading is a +process to allow PHP to automatically load classes on demand. For our Module we set this up by adding a +``getAutoloaderConfig()`` function to our Module class. (This function is defined in the `AutoloaderProviderInterface `_, +although the presence of the function is enough, actually implementing the interface is optional.) + +.. code-block:: php + :linenos: + :emphasize-lines: 5,9 + + array( + 'namespaces' => array( + // Autoload all classes from namespace 'Blog' from '/module/Blog/src/Blog' + __NAMESPACE__ => __DIR__ . '/src/' . __NAMESPACE__, + ) + ) + ); + } + + /** + * Returns configuration to merge with application configuration + * + * @return array|\Traversable + */ + public function getConfig() + { + return include __DIR__ . '/config/module.config.php'; + } + } + +Now this looks like a lot of change but don't be afraid. We've added an ``getAutoloaderConfig()`` function which provides +configuration for the ``Zend\Loader\StandardAutoloader``. This configuration tells the application that classes +in ``__NAMESPACE__`` (``Blog``) can be found inside ``__DIR__ . '/src/' . __NAMESPACE__`` (``/module/Blog/src/Blog``). + +The ``Zend\Loader\StandardAutoloader`` uses a PHP community driven standard called `PSR-0` `_. +Amongst other things, this standard defines a way for PHP to map class names to the file system. So with this +configured, the application knows that our ``Blog\Controller\ListController`` class should exist at +``/module/Blog/src/Blog/Controller/ListController.php``. + +If you refresh the browser now you'll see the same error, as even though we've configured the autoloader, we still need +to create the controller class. Let's create this file now: + +.. code-block:: php + :linenos: + + `_ in order to be 'dispatched' +(or run) by ZendFramework's MVC layer. ZendFramework provides some base controller implementation of it with +`AbstractActionController `_, +which we are going to use. Let's modify our controller now: + +.. code-block:: php + :linenos: + :emphasize-lines: 5,7 + + +

Blog\ListController::indexAction()

+ +Before we continue let us quickly take a look at where we placed this file. Note that view files are found within the +``/view`` subdirectory, not ``/src`` as they are not PHP class files, but template files for rendering HTML. The +following path however deserves some explanation but it's very simple. First we have the lowercased namespace. Followed +by the lowercased controller name without the appendix 'controller' and lastly comes the name of the action that we are +accessing, again without the appendix 'action'. All in all it looks like this: ``/view/{namespace}/{controller}/{action}.phtml``. +This has become a community standard but can potentionally be changed by you at any time. + +However creating this file alone is not enough and this brings as to the final topic of this part of the QuickStart. We +need to let the application know where to look for view files. We do this within our modules configuration file ``module.config.php``. + +.. code-block:: php + :linenos: + :emphasize-lines: 4-8 + + array( + 'template_path_stack' => array( + __DIR__ . '/../view', + ), + ), + 'controllers' => array( /** Controller Configuration */), + 'router' => array( /** Route Configuration */ ) + ); + +The above configuration tells the application that the folder ``/module/Blog/view`` has view files in it that match the +above described default scheme. It is important to note that with this you can not only ship view files for your module +but you can also overwrite view files from other modules. + +Reload your site now. Finally we are at a point where we see something different than an error being displayed. +Congratulations, not only have you created a simple "Hello World" style module, you also learned about many error +messages and their causes. If we didn't exhaust you too much, continue with our QuickStart and let's create a module +that actually does something. diff --git a/docs/languages/en/in-depth-guide/preparing-db-backend.rst b/docs/languages/en/in-depth-guide/preparing-db-backend.rst new file mode 100644 index 000000000..137a713a3 --- /dev/null +++ b/docs/languages/en/in-depth-guide/preparing-db-backend.rst @@ -0,0 +1,296 @@ +Preparing for different Database-Backends +========================================= + +In the previous chapter we have created a ``PostService`` that returns some data from blog posts. While this served +an easy to understand learning purpose it is quite impractical for real world applications. No one would want to modify +the source files each time a new post is added. But luckily we all know about databases. All we need to learn is how +to interact with databases from our ZF2 application. + +But there is a catch. There are many database backend systems, namely SQL and NoSQL databases. While in a real-world +you would probably jump right to the solution that fits you the most at the time being, it is a better practice to +create another layer in front of the actual database access that abstracts the database interaction. We call this the +**Mapper-Layer**. + + +What is database abstraction? +============================= + +The term "database abstraction" may sound quite confusing but this is actually a very simple thing. Consider a SQL and +a NoSQL database. Both have methods for CRUD (Create, Read, Update, Delete) operations. For example to query the +database against a given row in MySQL you'd do a ``mysqli_query('SELECT foo FROM bar')``. But using an ORM for MongoDB +for example you'd do something like ``$mongoODM->getRepository('bar')->find('foo')``. Both engines would give you the +same result but the execution is different. + +So if we start using a SQL database and write those codes directly into our ``PostService`` and a year later we decide +to switch to a NoSQL database, we would literally have to delete all previously coded lines and write new ones. And +in a few years later a new thing pops up and we have to delete and re-write codes again. This isn't really the best +approach and that's precisely where database abstraction or the Mapper-Layer comes in handy. + +Basically what we do is to create a new Interface. This interface then defines **how** our database interaction should +function but the actual implementation is left out. But let's stop the theory and go over to code this thing. + + +Creating the PostMapperInterface +================================ + +Let's first think a bit about what possible database interactions we can think of. We need to be able to: + +- find a single blog post +- find all blog posts +- insert new blog post +- update existing blog posts +- delete existing blog posts + +Those are the most important ones I'd guess for now. Considering ``insert()`` and ``update()`` both write into the +database it'd be nice to have just a single ``save()``-function that calls the proper function internally. + +Start by creating a new file inside a new namespace ``Blog\Mapper`` called ``PostMapperInterface.php`` and add the +following content to it. + +.. code-block:: php + :linenos: + + postMapper = $postMapper; + } + + /** + * {@inheritDoc} + */ + public function findAllPosts() + { + } + + /** + * {@inheritDoc} + */ + public function findPost($id) + { + } + } + +With this we now require an implementation of the ``PostMapperInterface`` for our ``PostService`` to function. Since +none exists yet we can not get our application to work and we'll be seeing the following PHP error: + +.. code-block:: text + :linenos: + + Catchable fatal error: Argument 1 passed to Blog\Service\PostService::__construct() + must implement interface Blog\Mapper\PostMapperInterface, none given, + called in {path}\module\Blog\src\Blog\Service\PostServiceFactory.php on line 19 + and defined in {path}\module\Blog\src\Blog\Service\PostService.php on line 17 + +But the power of what we're doing lies within assumptions that we **can** make. This ``PostService`` will always have +a mapper passed as an argument. So in our ``find*()``-functions we **can** assume that it is there. Recall that the +``PostMapperInterface`` defines a ``find($id)`` and a ``findAll()`` function. Let's use those within our +Service-functions: + +.. code-block:: php + :linenos: + :emphasize-lines: 27, 35 + + postMapper = $postMapper; + } + + /** + * {@inheritDoc} + */ + public function findAllPosts() + { + return $this->postMapper->findAll(); + } + + /** + * {@inheritDoc} + */ + public function findPost($id) + { + return $this->postMapper->find($id); + } + } + +Looking at this code you'll see that we use the ``postMapper`` to get access to the data we want. How this is happening +isn't the business of the ``PostService`` anymore. But the ``PostService`` does know what data it will receive and +that's the only important thing. + + +The PostService has a dependency +================================ + +Now that we have introduced the ``PostMapperInterface`` as a dependency for the ``PostService`` we are no longer able to +define this service as an ``invokable`` because it has a dependency. So we need to create a factory for the service. Do +this by creating a factory the same way we have done for the ``ListController``. First change the configuration from an +``invokables``-entry to a ``factories``-entry and assign the proper factory class: + +.. code-block:: php + :linenos: + :emphasize-lines: 4-8 + + array( + 'factories' => array( + 'Blog\Service\PostServiceInterface' => 'Blog\Factory\PostServiceFactory' + ) + ), + 'view_manager' => array( /** ViewManager Config */ ), + 'controllers' => array( /** ControllerManager Config */ ), + 'router' => array( /** Router Config */ ) + ); + +Going by the above configuration we now need to create the class ``Blog\Factory\PostServiceFactory`` so let's go ahead +and create it: + +.. code-block:: php + :linenos: + + get('Blog\Mapper\PostMapperInterface') + ); + } + } + +With this in place you should now be able to see the ``ServiceNotFoundException``, thrown by the ``ServiceManager``, +saying that the requested service cannot be found. + +.. code-block:: text + :linenos: + + Additional information: + Zend\ServiceManager\Exception\ServiceNotFoundException + File: + {libraryPath}\Zend\ServiceManager\ServiceManager.php:529 + Message: + Zend\ServiceManager\ServiceManager::get was unable to fetch or create an instance for Blog\Mapper\PostMapperInterface + +Conclusion +========== + +We finalize this chapter with the fact that we successfully managed to keep the database-logic outside of our service. +Now we are able to implement different database solution depending on our need and change them easily when the time +requires it. + +In the next chapter we will create the actual implementation of our ``PostMapperInterface`` using ``Zend\Db\Sql``. \ No newline at end of file diff --git a/docs/languages/en/in-depth-guide/review.rst b/docs/languages/en/in-depth-guide/review.rst new file mode 100644 index 000000000..ef885fa2a --- /dev/null +++ b/docs/languages/en/in-depth-guide/review.rst @@ -0,0 +1,94 @@ +Reviewing the Blog-application +=============================== + +Throughout the past seven chapters we have created a fully functional CRUD-Application using music-blogs as an example. +While doing so we've made use of several different design-patterns and best-practices. Now it's time to reiterate and +take a look at some of the code-samples we've written. This is going to be done in a Q&A fashion. + +- `Do we always need all the layers and interfaces?`_ +- `Having many objects, won't there be many code-duplication?`_ +- `Why are there so many controllers?`_ + + +Do we always need all the layers and interfaces? +------------------------------------------------ + +Short answer: no. + +Long answer: The importance of interfaces goes up the bigger your application becomes. If you can foresee that +your application will be used by other people or is supposed to be extendable, then you should strongly consider to +always code against interfaces. This is a very common best-practice that is not tied to ZF2 specifically but rather +aimed at strict OOP programming. + +The main role of the multiple layers that we have introduced ( **Controller** -> **Service** -> **Mapper** -> +**Backend** ) are to get a strict separation of concerns for all of our objects. There are many resources who can +explain in detail the big advantages of each layer so please go ahead and read up on them. + +For a very simple application, though, you're most likely to strip away the **Mapper**-layer. In practice all the code +from the mapper layer often resides inside the services directly. And this works for most of the applications but as +soon as you plan to support multiple backends (i.e. open source software) or you want to be prepared for changing +backends, you should always consider including this layer. + + +Having many objects, won't there be much code-duplication? +---------------------------------------------------------- + +Short answer: yes. + +Long answer: there doesn't need to be. Most code-duplication would come from the mapper-layer, too. If you take a +closer look at the class you'll notice that there's just two things that are tied to a specific object. First, it is +the name of the database-table. Second, it is the object-prototype that's passed into the mapper. + +The prototype is already passed into the class from the ``__construct()`` function so that's already interchangeable. +If you want to make the table-name interchangeable, too, all you need to do is to provide the table-name from the +constructor, too, and you have a fully versatile db-mapper-implementation that can be used for pretty much every +object of your application. + +You could then write a factory class that could look like this: + +.. code-block:: php + :linenos: + + get('Zend\Db\Adapter\Adapter'), // DB-Adapter + 'news', // Table-Name + new ClassMethods(false), // Object-Hydrator + new News() // Object-Prototype + ); + } + } + + +Why are there so many controllers? +---------------------------------- + +Looking back at code-examples from a couple of years back you'll notice that there was a lot of code inside each +controller. This has become a bad-practice that's known as Fat Controllers or Bloated Controllers. + +The major difference about each controller we have created is that there are different dependencies. For example, the +``WriteController`` required the ``PostForm`` as well as the ``PostService`` while the ``DeleteController`` only required the +``PostService``. In this example it wouldn't make sense to write the ``deleteAction()`` into the ``WriteController`` because +we then would needlessly create an instance of the ``PostForm`` which is not required. In large scale applications this +would create a huge bottleneck that would slow down the application. + +Looking at the ``DeleteController`` as well as the ``ListController`` you'll notice that both controllers have the same +dependency. Both require only the ``PostService`` so why not merge them into one controller? The reason here is for +semantical reasons. Would you look for a ``deleteAction()`` in a ``ListController``? Most of us wouldn't and therefore we +have created a new class for that. + +In applications where the ``InsertForm`` differs from the ``UpdateForm`` you'd always want to have two different controllers +for each of them instead of one united ``WriteController`` like we have in our example. These things heavily differ from +application to application but the general intent always is: **keep your controllers slim / lightweight**! + + +Do you have more questions? PR them! +------------------------------------ + +If there's anything you feel that's missing in this FAQ, please PR your question and we will give you the answer that +you need! diff --git a/docs/languages/en/in-depth-guide/services-and-servicemanager.rst b/docs/languages/en/in-depth-guide/services-and-servicemanager.rst new file mode 100644 index 000000000..f55ba2663 --- /dev/null +++ b/docs/languages/en/in-depth-guide/services-and-servicemanager.rst @@ -0,0 +1,672 @@ +Introducing Services and the ServiceManager +=========================================== + +In the previous chapter we've learned how to create a simple "Hello World" Application in Zend Framework 2. This is a +good start and easy to understand but the application itself doesn't really do anything. In this chapter we will +introduce you into the concept of Services and with this the introduction to ``Zend\ServiceManager\ServiceManager``. + +What is a Service? +================== + +A Service is an object that executes complex application logic. It's the part of the application that wires all +difficult stuff together and gives you easy to understand results. + +For what we're trying to accomplish with our ``Blog``-Module this means that we want to have a Service that will give +us the data that we want. The Service will get it's data from some source and when writing the Service we don't really +care about what the source actually is. The Service will be written against an ``Interface`` that we define and that +future Data-Providers have to implement. + +Writing the PostService +======================= + +When writing a Service it is a common best-practice to define an ``Interface`` first. ``Interfaces`` are a good way to +ensure that other programmers can easily build extensions for our Services using their own implementations. In other +words, they can write Services that have the same function names but internally do completely different things but have +the same specified result. + +In our case we want to create a ``PostService``. This means first we are going to define a ``PostServiceInterface``. +The task of our Service is to provide us with data of our blog posts. For now we are going to focus on the read-only +side of things. We will define a function that will give us all posts and we will define a function that will give us a +single post. + +Let's start by creating the Interface at ``/module/Blog/src/Blog/Service/PostServiceInterface.php`` + +.. code-block:: php + :linenos: + + id; + } + + /** + * @param int $id + */ + public function setId($id) + { + $this->id = $id; + } + + /** + * {@inheritDoc} + */ + public function getTitle() + { + return $this->title; + } + + /** + * @param string $title + */ + public function setTitle($title) + { + $this->title = $title; + } + + /** + * {@inheritDoc} + */ + public function getText() + { + return $this->text; + } + + /** + * @param string $text + */ + public function setText($text) + { + $this->text = $text; + } + } + +Bringing Life into our PostService +================================== + +Now that we have our Model files in place we can actually bring life into our ``PostService`` class. To keep the +Service-Layer easy to understand for now we will only return some hard-coded content from our ``PostService`` class directly. Create +a property inside the ``PostService`` called ``$data`` and make this an array of our Model type. Edit ``PostService`` like +this: + +.. code-block:: php + :linenos: + :emphasize-lines: 7-33 + + 1, + 'title' => 'Hello World #1', + 'text' => 'This is our first blog post!' + ), + array( + 'id' => 2, + 'title' => 'Hello World #2', + 'text' => 'This is our second blog post!' + ), + array( + 'id' => 3, + 'title' => 'Hello World #3', + 'text' => 'This is our third blog post!' + ), + array( + 'id' => 4, + 'title' => 'Hello World #4', + 'text' => 'This is our fourth blog post!' + ), + array( + 'id' => 5, + 'title' => 'Hello World #5', + 'text' => 'This is our fifth blog post!' + ) + ); + + /** + * {@inheritDoc} + */ + public function findAllPosts() + { + // TODO: Implement findAllPosts() method. + } + + /** + * {@inheritDoc} + */ + public function findPost($id) + { + // TODO: Implement findPost() method. + } + } + +After we now have some data, let's modify our ``find*()`` functions to return the appropriate model files: + +.. code-block:: php + :linenos: + :emphasize-lines: 42-48, 56-63 + + 1, + 'title' => 'Hello World #1', + 'text' => 'This is our first blog post!' + ), + array( + 'id' => 2, + 'title' => 'Hello World #2', + 'text' => 'This is our second blog post!' + ), + array( + 'id' => 3, + 'title' => 'Hello World #3', + 'text' => 'This is our third blog post!' + ), + array( + 'id' => 4, + 'title' => 'Hello World #4', + 'text' => 'This is our fourth blog post!' + ), + array( + 'id' => 5, + 'title' => 'Hello World #5', + 'text' => 'This is our fifth blog post!' + ) + ); + + /** + * {@inheritDoc} + */ + public function findAllPosts() + { + $allPosts = array(); + + foreach ($this->data as $index => $post) { + $allPosts[] = $this->findPost($index); + } + + return $allPosts; + } + + /** + * {@inheritDoc} + */ + public function findPost($id) + { + $postData = $this->data[$id]; + + $model = new Post(); + $model->setId($postData['id']); + $model->setTitle($postData['title']); + $model->setText($postData['text']); + + return $model; + } + } + +As you can see, both our functions now have appropriate return values. Please note that from a technical point of view +the current implementation is far from perfect. We will improve this Service a lot in the future but for now we have +a working Service that is able to give us some data in a way that is defined by our ``PostServiceInterface``. + + +Bringing the Service into the Controller +======================================== + +Now that we have our ``PostService`` written, we want to get access to this Service in our Controllers. For this task +we will step foot into a new topic called "Dependency Injection", short "DI". + +When we're talking about dependency injection we're talking about a way to get dependencies into our classes. The most +common form, "Constructor Injection", is used for all dependencies that are required by a class at all times. + +In our case we want to have our Blog-Modules ``ListController`` somehow interact with our ``PostService``. This means +that the class ``PostService`` is a dependency of the class ``ListController``. Without the ``PostService`` our +``ListController`` will not be able to function properly. To make sure that our ``ListController`` will always get the +appropriate dependency, we will first define the dependency inside the ``ListControllers`` constructor function +``__construct()``. Go on and modify the ``ListController`` like this: + +.. code-block:: php + :linenos: + :emphasize-lines: 5, 8, 13, 15-18 + + postService = $postService; + } + } + +As you can see our ``__construct()`` function now has a required argument. We will not be able to call this class anymore +without passing it an instance of a class that matches our defined ``PostServiceInterface``. If you were to go back to +your browser and reload your project with the url ``localhost:8080/blog``, you'd see the following error message: + +.. code-block:: text + :linenos: + + ( ! ) Catchable fatal error: Argument 1 passed to Blog\Controller\ListController::__construct() + must be an instance of Blog\Service\PostServiceInterface, none given, + called in {libraryPath}\Zend\ServiceManager\AbstractPluginManager.php on line {lineNumber} + and defined in \module\Blog\src\Blog\Controller\ListController.php on line 15 + +And this error message is expected. It tells you exactly that our ``ListController`` expects to be passed an implementation +of the ``PostServiceInterface``. So how do we make sure that our ``ListController`` will receive such an implementation? +To solve this, we need to tell the application how to create instances of the ``Blog\Controller\ListController``. If you +remember back to when we created the controller, we added an entry to the ``invokables`` array in the module config: + +.. code-block:: php + :linenos: + :emphasize-lines: 6-8 + + array( /** ViewManager Config */ ), + 'controllers' => array( + 'invokables' => array( + 'Blog\Controller\List' => 'Blog\Controller\ListController' + ) + ), + 'router' => array( /** Router Config */ ) + ); + +An ``invokable`` is a class that can be constructed without any arguments. Since our ``Blog\Controller\ListController`` +now has a required argument, we need to change this. The ``ControllerManager``, which is responsible for instantiating +controllers, also support using ``factories``. A ``factory`` is a class that creates instances of another class. +We'll now create one for our ``ListController``. Let's modify our configuration like this: + + +.. code-block:: php + :linenos: + :emphasize-lines: 6-8 + + array( /** ViewManager Config */ ), + 'controllers' => array( + 'factories' => array( + 'Blog\Controller\List' => 'Blog\Factory\ListControllerFactory' + ) + ), + 'router' => array( /** Router Config */ ) + ); + +As you can see we no longer have the key ``invokables``, instead we now have the key ``factories``. Furthermore the value +of our controller name ``Blog\Controller\List`` has been changed to not match the class ``Blog\Controller\ListController`` +directly but to rather call a class called ``Blog\Factory\ListControllerFactory``. If you refresh your browser +you'll see a different error message: + +.. code-block:: html + :linenos: + + An error occurred + An error occurred during execution; please try again later. + + Additional information: + Zend\ServiceManager\Exception\ServiceNotCreatedException + + File: + {libraryPath}\Zend\ServiceManager\AbstractPluginManager.php:{lineNumber} + + Message: + While attempting to create blogcontrollerlist(alias: Blog\Controller\List) an invalid factory was registered for this instance type. + +This message should be quite easy to understand. The ``Zend\Mvc\Controller\ControllerManager`` +is accessing ``Blog\Controller\List``, which internally is saved as ``blogcontrollerlist``. While it does so it notices +that a factory class is supposed to be called for this controller name. However, it doesn't find this factory class so +to the Manager it is an invalid factory. Using easy words: the Manager doesn't find the Factory class so that's probably +where our error lies. And of course, we have yet to write the factory, so let's go ahead and do this. + + +Writing a Factory Class +======================= + +Factory classes within Zend Framework 2 always need to implement the ``Zend\ServiceManager\FactoryInterface``. +Implementing this class lets the ServiceManager know that the function ``createService()`` is supposed to be called. And +``createService()`` actually expects to be passed an instance of the `ServiceLocatorInterface` so the `ServiceManager` will +always inject this using Dependency Injection as we have learned above. Let's implement our factory class: + +.. code-block:: php + :linenos: + + getServiceLocator(); + $postService = $realServiceLocator->get('Blog\Service\PostServiceInterface'); + + return new ListController($postService); + } + } + +Now this looks complicated! Let's start to look at the ``$realServiceLocator``. When using a Factory-Class that will be +called from the ``ControllerManager`` it will actually inject **itself** as the ``$serviceLocator``. However, we need the real +``ServiceManager`` to get to our Service-Classes. This is why we call the function ``getServiceLocator()` who will give us +the real ``ServiceManager``. + +After we have the ``$realServiceLocator`` set up we try to get a Service called ``Blog\Service\PostServiceInterface``. +This name that we're accessing is supposed to return a Service that matches the ``PostServiceInterface``. This Service +is then passed along to the ``ListController`` which will directly be returned. + +Note though that we have yet to register a Service called ``Blog\Service\PostServiceInterface``. There's no magic +happening that does this for us just because we give the Service the name of an Interface. Refresh your browser and you +will see this error message: + +.. code-block:: text + :linenos: + + An error occurred + An error occurred during execution; please try again later. + + Additional information: + Zend\ServiceManager\Exception\ServiceNotFoundException + + File: + {libraryPath}\Zend\ServiceManager\ServiceManager.php:{lineNumber} + + Message: + Zend\ServiceManager\ServiceManager::get was unable to fetch or create an instance for Blog\Service\PostServiceInterface + +Exactly what we expected. Somewhere in our application - currently our factory class - a service called +``Blog\Service\PostServiceInterface`` is requested but the ``ServiceManager`` doesn't know about this Service yet. +Therefore it isn't able to create an instance for the requested name. + + +Registering Services +==================== + +Registering a Service is as simple as registering a Controller. All we need to do is modify our ``module.config.php`` and +add a new key called ``service_manager`` that then has ``invokables`` and ``factories``, too, the same way like we have it +inside our ``controllers`` array. Check out the new configuration file: + +.. code-block:: php + :linenos: + :emphasize-lines: 4-8 + + array( + 'invokables' => array( + 'Blog\Service\PostServiceInterface' => 'Blog\Service\PostService' + ) + ), + 'view_manager' => array( /** View Manager Config */ ), + 'controllers' => array( /** Controller Config */ ), + 'router' => array( /** Router Config */ ) + ); + +As you can see we now have added a new Service that listens to the name ``Blog\Service\PostServiceInterface`` and +points to our own implementation which is ``Blog\Service\PostService``. Since our Service has no dependencies we are +able to add this Service under the ``invokables`` array. Try refreshing your browser. You should see no more error +messages but rather exactly the page that we have created in the previous chapter of the Tutorial. + +Using the Service at our Controller +=================================== + +Let's now use the ``PostService`` within our ``ListController``. For this we will need to overwrite the default +``indexAction()`` and return the values of our ``PostService`` into the view. Modify the ``ListController`` like this: + +.. code-block:: php + :linenos: + :emphasize-lines: 6, 23-25 + + postService = $postService; + } + + public function indexAction() + { + return new ViewModel(array( + 'posts' => $this->postService->findAllPosts() + )); + } + } + +First please note the our controller imported another class. We need to import ``Zend\View\Model\ViewModel``, which +usually is what your Controllers will return. When returning an instance of a ``ViewModel`` you're able to always +assign so called View-Variables. In this case we have assigned a variable called ``$posts`` with the value of whatever +the function ``findAllPosts()`` of our ``PostService`` returns. In our case it is an array of ``Blog\Model\Post`` classes. +Refreshing the browser won't change anything yet because we obviously need to modify our view-file to be able to display +the data we want to. + +.. note:: + + You do not actually need to return an instance of ``ViewModel``. When you return a normal php ``array`` it will + internally be converted into a ``ViewModel``. So in short: + + ``return new ViewModel(array('foo' => 'bar'));`` + + equals + + ``return array('foo' => 'bar');`` + + +Accessing View Variables +======================== + +When pushing variables to the view they are accessible through two ways. Either directly like ``$this->posts`` or +implicitly like ``$posts``. Both are the same, however, calling ``$posts`` implicitly will result in a little round-trip +through the ``__call()`` function. + +Let's modify our view to display a table of all blog posts that our ``PostService`` returns. + +.. code-block:: php + :linenos: + :emphasize-lines: 13, 15-17, 19 + + +

Blog

+ + posts as $post): ?> +
+

getTitle() ?>

+

+ getText() ?> +

+
+ + +In here we simply define a little HTML-Table and then run a ``foreach`` over the array ``$this->posts``. Since every +single entry of our array is of type ``Blog\Model\Post`` we can use the respective getter functions to receive the data +we want to get. + +Summary +======= + +And with this the current chapter is finished. We now have learned how to interact with the ``ServiceManager`` and we +also know what dependency injection is all about. We are now able to pass variables from our services into the view +through a controller and we know how to iterate over arrays inside a view-script. + +In the next chapter we will take a first look at the things we should do when we want to get data from a database. diff --git a/docs/languages/en/in-depth-guide/understanding-routing.rst b/docs/languages/en/in-depth-guide/understanding-routing.rst new file mode 100644 index 000000000..18600b25d --- /dev/null +++ b/docs/languages/en/in-depth-guide/understanding-routing.rst @@ -0,0 +1,602 @@ +Understanding the Router +======================== + +Right now we have a pretty solid set up for our module. However, we're not really doing all too much yet, to be +precise, all we do is display all ``Blog`` entries on one page. In this chapter you will learn everything you need +to know about the ``Router`` to create other routes to be able to display only a single blog, to add new blogs +to your application and to edit and delete existing blogs. + + +Different route types +===================== + +Before we go into details on our application, let's take a look at the most important route types that Zend +Framework offers. + +Zend\\Mvc\\Router\\Http\\Literal +-------------------------------- + +The first common route type is the ``Literal``-Route. As mentioned in a previous chapter a literal route is one that +matches a specific string. Examples for URLs that are usually literal routes are: + +- http://domain.com/blog +- http://domain.com/blog/add +- http://domain.com/about-me +- http://domain.com/my/very/deep/page +- http://domain.com/my/very/deep/page + +Configuration for a literal route requires you to set up the route that should be matched and needs you to define +some defaults to be used, for example which controller and which action to call. A simple configuration for a +literal route looks like this: + +.. code-block:: php + :linenos: + :emphasize-lines: 3, 4, 6, 8, 9 + + 'router' => array( + 'routes' => array( + 'about' => array( + 'type' => 'literal', + 'options' => array( + 'route' => '/about-me', + 'defaults' => array( + 'controller' => 'AboutMeController', + 'action' => 'aboutme', + ), + ), + ) + ) + ) + +Zend\\Mvc\\Router\\Http\\Segment +-------------------------------- + +The second most commonly used route type is the ``Segment``-Route. A segmented route is used for whenever your url +is supposed to contain variable parameters. Pretty often those parameters are used to identify certain objects +within your application. Some examples for URLs that contain parameters and are usually segment routes are: + +.. code-block:: text + :lineos: + + http://domain.com/blog/1 // parameter "1" + http://domain.com/blog/details/1 // parameter "1" + http://domain.com/blog/edit/1 // parameter "1" + http://domain.com/blog/1/edit // parameter "1" + http://domain.com/news/archive/2014 // parameter "2014" + http://domain.com/news/archive/2014/january // parameter "2014" and "january" + +Configuring a ``Segment``-Route takes a little more effort but isn't difficult to understand. The tasks you have to +do are similar at first, you have to define the route-type, just be sure to make it ``Segment``. Then you have to +define the route and add parameters to it. Then as usual you define the defaults to be used, the only thing that +differs in this part is that you can assign defaults for your parameters, too. The new part that is used on routes +of the ``Segment`` type is to define so called ``constraints``. They are used to tell the ``Router`` what "rules" are +given for parameters. For example, an ``id``-parameter is only allowed to be of type ``integer``, the ``year``-parameter +is only allowed to be of type ``integer`` and may only contain exactly ``four digits``. A sample configuration can +look like this: + +.. code-block:: php + :linenos: + :emphasize-lines: 4, 6, 11-13 + + 'router' => array( + 'routes' => array( + 'archives' => array( + 'type' => 'segment', + 'options' => array( + 'route' => '/news/archive/:year', + 'defaults' => array( + 'controller' => 'ArchiveController', + 'action' => 'byYear', + ), + 'constraints' => array( + 'year' => '\d{4}' + ) + ), + ) + ) + ) + +This configuration defines a route for a URL like ``domain.com/news/archive/2014``. As you can see we our route now +contains the part ``:year``. This is called a route-parameter. Route parameters for ``Segment``-Routes are defined by a +in front of a string. The string then is the ``name`` of the parameter. + +Under ``constraints`` you see that we have another array. This array contains regular expression rules for each +parameter of your route. In our example case the regex uses two parts, the first one being ``\d`` which means "a +digit", so any number from 0-9. The second part is ``{4}`` which means that the part before this has to match exactly +four times. So in easy words we say "four digits". + +If now you call the URL ``domain.com/news/archive/123``, the router will not match the URL because we only support +years with four digits. + +You may notice that we did not define any ``defaults`` for the parameter ``year``. This is because the parameter is +currently set up as a ``required`` parameter. If a parameter is supposed to be ``optional`` we need to define this +inside the route definition. This is done by adding square brackets around the parameter. Let's modify the above +example route to have the ``year`` parameter optional and use the current year as default: + +.. code-block:: php + :linenos: + :emphasize-lines: 10 + + 'router' => array( + 'routes' => array( + 'archives' => array( + 'type' => 'segment', + 'options' => array( + 'route' => '/news/archive[/:year]', + 'defaults' => array( + 'controller' => 'ArchiveController', + 'action' => 'byYear', + 'year' => date('Y') + ), + 'constraints' => array( + 'year' => '\d{4}' + ) + ), + ) + ) + ) + +Notice that now we have a part in our route that is optional. Not only the parameter ``year`` is optional. The slash +that is separating the ``year`` parameter from the URL string ``archive`` is optional, too, and may only be there +whenever the ``year`` parameter is present. + + +Different routing concepts +========================== + +When thinking about the whole application it becomes clear that there are a lot of routes to be matched. When +writing these routes you have two options. One option is to spend less time writing routes that in turn +are a little slow in matching. Another option is to write very explicit routes that match a little faster +but require more work to define. Let's take a look at both of them. + +Generic routes +-------------- + +A generic route is one that matches many URLs. You may remember this concept from Zend Framework 1 where basically +you didn't even bother about routes because we had one "god route" that was used for everything. You define the +controller, the action, and all parameters within just one single route. + +The big advantage of this approach is the immense time you save when developing your application. The downside, +however, is that matching such a route can take a little bit longer due to the fact that so many variables need to +be checked. However, as long as you don't overdo it, this is a viable concept. For this reason the +ZendSkeletonApplication uses a very generic route, too. Let's take a look at a generic route: + +.. code-block:: php + :linenos: + :emphasize-lines: 4, 6, 8-10, 13, 14 + + 'router' => array( + 'routes' => array( + 'default' => array( + 'type' => 'segment', + 'options' => array( + 'route' => '/[:controller[/:action]]', + 'defaults' => array( + '__NAMESPACE__' => 'Application\Controller', + 'controller' => 'Index', + 'action' => 'index', + ), + 'constraints' => [ + 'controller' => '[a-zA-Z][a-zA-Z0-9_-]*', + 'action' => '[a-zA-Z][a-zA-Z0-9_-]*', + ] + ), + ) + ) + ) + +Let's take a closer look as to what has been defined in this configuration. The ``route`` part now contains two +optional parameters, ``controller`` and ``action``. The ``action`` parameter is optional only when the ``controller`` +parameter is present. + +Within the ``defaults``-section it looks a little bit different, too. The ``__NAMESPACE__`` will be used to concatenate +with the ``controller`` parameter at all times. So for example when the ``controller`` parameter is "news" then the +``controller`` to be called from the ``Router`` will be ``Application\Controller\news``, if the parameter is "archive" +the ``Router`` will call the controller ``Application\Controller\archive``. + +The ``defaults``-section then is pretty straight forward again. Both parameters, ``controller`` and ``action``, only +have to follow the conventions given by PHP-Standards. They have to start with a letter from ``a-z``, upper- or +lowercase and after that first letter there can be an (almost) infinite amount of letters, digits, underscores or +dashes. + +**The big downside** to this approach not only is that matching this route is a little slower, it is that there +is no error-checking going on. For example, when you were to call a URL like ``domain.com/weird/doesntExist`` then +the ``controller`` would be "Application\\Controller\\weird" and the ``action`` would be "doesntExistAction". As you can +guess by the names let's assume neither ``controller`` nor ``action`` does exist. The route will still match but an +``Exception`` will be thrown because the ``Router`` will be unable to find the requested resources and we'll receive +a ``404``-Response. + + +Explicit routes using child_routes +---------------------------------- + +Explicit routing is done by defining all possible routes yourself. For this method you actually have two options +available, too. + +**Without config structure** + +The probably most easy to understand way to write explicit routes would be to write many top level routes like +in the following configuration: + +.. code-block:: php + :linenos: + :emphasize-lines: + + 'router' => array( + 'routes' => array( + 'news' => array( + 'type' => 'literal', + 'options' => array( + 'route' => '/news', + 'defaults' => array( + 'controller' => 'NewsController', + 'action' => 'showAll', + ), + ), + ), + 'news-archive' => array( + 'type' => 'segment', + 'options' => array( + 'route' => '/news/archive[/:year]', + 'defaults' => array( + 'controller' => 'NewsController', + 'action' => 'archive', + ), + 'constraints' => array( + 'year' => '\d{4}' + ) + ), + ), + 'news-single' => array( + 'type' => 'segment', + 'options' => array( + 'route' => '/news/:id', + 'defaults' => array( + 'controller' => 'NewsController', + 'action' => 'detail', + ), + 'constraints' => array( + 'id' => '\d+' + ) + ), + ), + ) + ) + +As you can see with this little example, all routes have an explicit name and there's lots of repetition going on. +We have to redefine the default ``controller`` to be used every single time and we don't really have any structure +within the configuration. Let's take a look at how we could bring more structure into a configuration like this. + +**Using child_routes for more structure** + +Another option to define explicit routes is to be using ``child_routes``. Child routes inherit all ``options`` from +their respective parents. Meaning: when the ``controller`` doesn't change, you do not need to redefine it. Let's take +a look at a child routes configuration using the same example as above: + +.. code-block:: php + :linenos: + :emphasize-lines: 13, 14 + + 'router' => array( + 'routes' => array( + 'news' => array( + 'type' => 'literal', + 'options' => array( + 'route' => '/news', + 'defaults' => array( + 'controller' => 'NewsController', + 'action' => 'showAll', + ), + ), + // Defines that "/news" can be matched on its own without a child route being matched + 'may_terminate' => true, + 'child_routes' => array( + 'archive' => array( + 'type' => 'segment', + 'options' => array( + 'route' => '/archive[/:year]', + 'defaults' => array( + 'action' => 'archive', + ), + 'constraints' => array( + 'year' => '\d{4}' + ) + ), + ), + 'single' => array( + 'type' => 'segment', + 'options' => array( + 'route' => '/:id', + 'defaults' => array( + 'action' => 'detail', + ), + 'constraints' => array( + 'id' => '\d+' + ) + ), + ), + ) + ), + ) + ) + +This routing configuration requires a little more explanation. First of all we have a new configuration entry which +is called ``may_terminate``. This property defines that the parent route can be matched alone, without child routes +needing to be matched, too. In other words all of the following routes are valid: + +- /news +- /news/archive +- /news/archive/2014 +- /news/42 + +If, however, you were to set ``may_terminate => false``, then the parent route would only be used for global defaults +that all ``child_routes`` were to inherit. In other words: only ``child_routes`` can be matched, so the only valid +routes would be: + +- /news/archive +- /news/archive/2014 +- /news/42 + +The parent route would not be able to be matched on its own. + +Next to that we have a new entry called ``child_routes``. In here we define new routes that will be appended to the +parent route. There's no real difference in configuration from routes you define as a child route to routes that +are on the top level of the configuration. The only thing that may fall away is the re-definition of shared +default values. + +The big advantage you have with this kind of configuration is the fact that you explicitly define the routes and +therefore you will never run into problems of non-existing controllers like you would with generic routes like +described above. The second advantage would be that this kind of routing is a little bit faster than generic routes +and the last advantage would be that you can easily see all possible URLs that start with ``/news``. + +While ultimately this falls into the category of personal preference bare in mind that debugging of explicit routes +is significantly easier than debugging generic routes. + + +A practical example for our Blog Module +======================================= + +Now that we know how to configure new routes, let's first create a route to display only a single ``Blog`` from our +Database. We want to be able to identify blog posts by their internal ID. Given that ID is a variable parameter we need +a route of type ``Segment``. Furthermore we want to put this route as a child route to the route of name ``blog``. + +.. code-block:: php + :linenos: + :emphasize-lines: 8-36 + + array( /** DB Config */ ), + 'service_manager' => array( /* ServiceManager Config */ ), + 'view_manager' => array( /* ViewManager Config */ ), + 'controllers' => array( /* ControllerManager Config */ ), + 'router' => array( + 'routes' => array( + 'blog' => array( + 'type' => 'literal', + 'options' => array( + 'route' => '/blog', + 'defaults' => array( + 'controller' => 'Blog\Controller\List', + 'action' => 'index', + ), + ), + 'may_terminate' => true, + 'child_routes' => array( + 'detail' => array( + 'type' => 'segment', + 'options' => array( + 'route' => '/:id', + 'defaults' => array( + 'action' => 'detail' + ), + 'constraints' => array( + 'id' => '[1-9]\d*' + ) + ) + ) + ) + ) + ) + ) + ); + +With this we have set up a new route that we use to display a single blog entry. We have assigned a parameter +called ``id`` that needs to be a positive digit excluding 0. Database entries usually start with a 0 when it comes +to primary ID keys and therefore our regular expression ``constraints`` for the ``id`` fields looks a little more +complicated. Basically we tell the router that the parameter ``id`` has to start with an integer between 1 and 9, +that's the ``[1-9]`` part, and after that any digit can follow, but doesn't have to (that's the ``\d*`` part). + +The route will call the same ``controller`` like the parent route but it will call the ``detailAction()`` instead. Go +to your browser and request the URL ``http://localhost:8080/blog/2``. You'll see the following error message: + +.. code-block:: text + :linenos: + + A 404 error occurred + + Page not found. + The requested controller was unable to dispatch the request. + + Controller: + Blog\Controller\List + + No Exception available + +This is due to the fact that the controller tries to access the ``detailAction()`` which does not yet exist. Let's go +ahead and create this action now. Go to your ``ListController`` and add the action. Return an empty ``ViewModel`` and +then refresh the page. + +.. code-block:: php + :linenos: + :emphasize-lines: 28-31 + + postService = $postService; + } + + public function indexAction() + { + return new ViewModel(array( + 'posts' => $this->postService->findAllPosts() + )); + } + + public function detailAction() + { + return new ViewModel(); + } + } + +Now you'll see the all familiar message that a template was unable to be rendered. Let's create this template now +and assume that we will get one ``Post``-Object passed to the template to see the details of our blog. Create a new +view file under ``/view/blog/list/detail.phtml``: + +.. code-block:: html + :linenos: + + +

Post Details

+ +
+
Post Title
+
escapeHtml($this->post->getTitle());?>
+
Post Text
+
escapeHtml($this->post->getText());?>
+
+ +Looking at this template we're expecting the variable ``$this->blog`` to be an instance of our ``Blog``-Model. Let's +now modify our ``ListController`` so that an ``Blog`` will be passed. + +.. code-block:: php + :linenos: + :emphasize-lines: 30-34 + + postService = $postService; + } + + public function indexAction() + { + return new ViewModel(array( + 'posts' => $this->postService->findAllPosts() + )); + } + + public function detailAction() + { + $id = $this->params()->fromRoute('id'); + + return new ViewModel(array( + 'post' => $this->postService->findPost($id) + )); + } + } + +If you refresh your application now you'll see the details for our ``Post`` to be displayed. However, there is one +little Problem with what we have done. While we do have our Service set up to throw an ``\InvalidArgumentException`` +whenever no ``Post`` matching a given ``id`` is found, we don't make use of this just yet. Go to your browser and +open the URL ``http://localhost:8080/blog/99``. You will see the following error message: + +.. code-block:: text + :linenos: + + An error occurred + An error occurred during execution; please try again later. + + Additional information: + InvalidArgumentException + + File: + {rootPath}/module/Blog/src/Blog/Service/PostService.php:40 + + Message: + Could not find row 99 + +This is kind of ugly, so our ``ListController`` should be prepared to do something whenever an +``InvalidArgumentException`` is thrown by the ``PostService``. Whenever an invalid ``Post`` is requested we want the +User to be redirected to the Post-Overview. Let's do this by putting the call against the ``PostService`` in a +try-catch statement. + +.. code-block:: php + :linenos: + :emphasize-lines: 30-40 + + postService = $postService; + } + + public function indexAction() + { + return new ViewModel(array( + 'posts' => $this->postService->findAllPosts() + )); + } + + public function detailAction() + { + $id = $this->params()->fromRoute('id'); + + try { + $post = $this->postService->findPost($id); + } catch (\InvalidArgumentException $ex) { + return $this->redirect()->toRoute('blog'); + } + + return new ViewModel(array( + 'post' => $post + )); + } + } + +Now whenever you access an invalid ``id`` you'll be redirected to the route ``blog`` which is our list of blog posts, +perfect! diff --git a/docs/languages/en/in-depth-guide/zend-db-sql-zend-stdlib-hydrator.rst b/docs/languages/en/in-depth-guide/zend-db-sql-zend-stdlib-hydrator.rst new file mode 100644 index 000000000..2e52f68fd --- /dev/null +++ b/docs/languages/en/in-depth-guide/zend-db-sql-zend-stdlib-hydrator.rst @@ -0,0 +1,843 @@ +Introducing Zend\\Db\\Sql and Zend\\Stdlib\\Hydrator +==================================================== + +In the last chapter we have introduced the mapping layer and created the ``PostMapperInterface``. Now it is time to +create an implementation of this interface so that we can make use of our ``PostService`` again. As an introductionary +example we will be using the ``Zend\Db\Sql`` classes. So let's jump right into it. + + +Preparing the Database +====================== + +Before we can start using a database we should prepare one. In this example we'll be using a MySQL-Database called +``blog`` which is accessible on the ``localhost``. The database will have one table called ``posts`` with three columns +``id``, ``title`` and ``text`` with the ``id`` being the primary key. For demo purpose, please use this database-dump. + +.. code-block:: sql + :linenos: + + CREATE TABLE posts ( + id int(11) NOT NULL auto_increment, + title varchar(100) NOT NULL, + text TEXT NOT NULL, + PRIMARY KEY (id) + ); + + INSERT INTO posts (title, text) + VALUES ('Blog #1', 'Welcome to my first blog post'); + INSERT INTO posts (title, text) + VALUES ('Blog #2', 'Welcome to my second blog post'); + INSERT INTO posts (title, text) + VALUES ('Blog #3', 'Welcome to my third blog post'); + INSERT INTO posts (title, text) + VALUES ('Blog #4', 'Welcome to my fourth blog post'); + INSERT INTO posts (title, text) + VALUES ('Blog #5', 'Welcome to my fifth blog post'); + + +Quick Facts Zend\\Db\\Sql +========================= + +To create queries against a database using ``Zend\Db\Sql`` you need to have a database connection available. This +connection is served through any class implementing the ``Zend\Db\Adapter\AdapterInterface``. The most handy way to +create such a class is through the use of the ``Zend\Db\Adapter\AdapterServiceFactory`` which listens to the config-key +``db``. Let's start by creating the required configuration entries and modify your ``module.config.php`` adding a new +top-level key called ``db``: + +.. code-block:: php + :linenos: + :emphasize-lines: 4-12 + + array( + 'driver' => 'Pdo', + 'username' => 'SECRET_USERNAME', //edit this + 'password' => 'SECRET_PASSWORD', //edit this + 'dsn' => 'mysql:dbname=blog;host=localhost', + 'driver_options' => array( + \PDO::MYSQL_ATTR_INIT_COMMAND => 'SET NAMES \'UTF8\'' + ) + ), + 'service_manager' => array( /** ServiceManager Config */ ), + 'view_manager' => array( /** ViewManager Config */ ), + 'controllers' => array( /** ControllerManager Config */ ), + 'router' => array( /** Router Config */ ) + ); + +As you can see we've added the ``db``-key and inside we create the parameters required to create a driver instance. + +.. note:: + + One important thing to note is that in general you **do not** want to have your credentials inside the normal + configuration file but rather in a local configuration file like ``/config/autoload/db.local.php``, that will + **not** be pushed to servers using zend-skeletons ``.gitignore`` file. Keep this in mind when you share your codes! + + Taking this example you would have this file: + + .. code-block:: php + :linenos: + + array( + 'driver' => 'Pdo', + 'username' => 'SECRET_USERNAME', //edit this + 'password' => 'SECRET_PASSWORD', //edit this + 'dsn' => 'mysql:dbname=blog;host=localhost', + 'driver_options' => array( + \PDO::MYSQL_ATTR_INIT_COMMAND => 'SET NAMES \'UTF8\'' + ) + ), + ); + +The next thing we need to do is by making use of the ``AdapterServiceFactory``. This is a ``ServiceManager`` entry that +will look like the following: + + +.. code-block:: php + :linenos: + :emphasize-lines: 16 + + array( + 'driver' => 'Pdo', + 'username' => 'SECRET_USERNAME', //edit this + 'password' => 'SECRET_PASSWORD', //edit this + 'dsn' => 'mysql:dbname=blog;host=localhost', + 'driver_options' => array( + \PDO::MYSQL_ATTR_INIT_COMMAND => 'SET NAMES \'UTF8\'' + ) + ), + 'service_manager' => array( + 'factories' => array( + 'Blog\Service\PostServiceInterface' => 'Blog\Service\Factory\PostServiceFactory', + 'Zend\Db\Adapter\Adapter' => 'Zend\Db\Adapter\AdapterServiceFactory' + ) + ), + 'view_manager' => array( /** ViewManager Config */ ), + 'controllers' => array( /** ControllerManager Config */ ), + 'router' => array( /** Router Config */ ) + ); + +Note the new Service that we called ``Zend\Db\Adapter\Adapter``. Calling this Service will now always give back a +running instance of the ``Zend\Db\Adapter\AdapterInterface`` depending on what driver we assign. + +With the adapter in place we're now able to run queries against the database. The construction of queries is best done +through the "QueryBuilder" features of ``Zend\Db\Sql`` which are ``Zend\Db\Sql\Sql`` for select queries, +``Zend\Db\Sql\Insert`` for insert queries, ``Zend\Db\Sql\Update`` for update queries and ``Zend\Db\Sql\Delete`` for +delete queries. The basic workflow of these components is: + +1. Build a query using ``Sql``, ``Insert``, ``Update`` or ``Delete`` +2. Create an Sql-Statement from the ``Sql`` object +3. Execute the query +4. Do something with the result + +Knowing this we can now write the implementation for the ``PostMapperInterface``. + + +Writing the mapper implementation +================================= + +Our mapper implementation will reside inside the same namespace as its interface. Go ahead and create a class called +``ZendDbSqlMapper`` and implement the ``PostMapperInterface``. + +.. code-block:: php + :linenos: + :emphasize-lines: + + dbAdapter = $dbAdapter; + } + + /** + * @param int|string $id + * + * @return PostInterface + * @throws \InvalidArgumentException + */ + public function find($id) + { + } + + /** + * @return array|PostInterface[] + */ + public function findAll() + { + } + } + +As you know from previous chapters, whenever we have a required parameter we need to write a factory for the class. Go +ahead and create a factory for our mapper implementation. + +.. code-block:: php + :linenos: + :emphasize-lines: + + get('Zend\Db\Adapter\Adapter') + ); + } + } + +We're now able to register our mapper implementation as a service. If you recall from the previous chapter, or if you +were to look at the current error message, you'll note that we call the Service ``Blog\Mapper\PostMapperInterface`` to +get a mapper implementation. Modify the configuration so that this key will call the newly called factory class. + +.. code-block:: php + :linenos: + :emphasize-lines: 7 + + array( /** Db Config */ ), + 'service_manager' => array( + 'factories' => array( + 'Blog\Mapper\PostMapperInterface' => 'Blog\Factory\ZendDbSqlMapperFactory', + 'Blog\Service\PostServiceInterface' => 'Blog\Service\Factory\PostServiceFactory', + 'Zend\Db\Adapter\Adapter' => 'Zend\Db\Adapter\AdapterServiceFactory' + ) + ), + 'view_manager' => array( /** ViewManager Config */ ), + 'controllers' => array( /** ControllerManager Config */ ), + 'router' => array( /** Router Config */ ) + ); + +With the adapter in place you're now able to refresh the blog index at ``localhost:8080/blog`` and you'll notice that +the ``ServiceNotFoundException`` is gone and we get the following PHP Warning: + +.. code-block:: text + :linenos: + + Warning: Invalid argument supplied for foreach() in /module/Blog/view/blog/list/index.phtml on line 13 + ID Text Title + +This is due to the fact that our mapper doesn't return anything yet. Let's modify the ``findAll()`` function to return +all blogs from the database table. + +.. code-block:: php + :linenos: + :emphasize-lines: 37-43 + + dbAdapter = $dbAdapter; + } + + /** + * @param int|string $id + * + * @return \Blog\Entity\PostInterface + * @throws \InvalidArgumentException + */ + public function find($id) + { + } + + /** + * @return array|\Blog\Entity\PostInterface[] + */ + public function findAll() + { + $sql = new Sql($this->dbAdapter); + $select = $sql->select('posts'); + + $stmt = $sql->prepareStatementForSqlObject($select); + $result = $stmt->execute(); + + return $result; + } + } + +The above code should look fairly straight forward to you. Sadly, though, a refresh of the application reveals another +error message. + +.. code-block:: text + :lineos: + + Fatal error: Call to a member function getId() on a non-object in /module/Blog/view/blog/list/index.phtml on line 15 + +Let's not return the ``$result`` variable for now and do a dump of it to see what we get here. Change the ``findAll()`` +function and do a data dumping of the ``$result`` variable: + +.. code-block:: php + :linenos: + :emphasize-lines: 45 + + dbAdapter = $dbAdapter; + } + + /** + * @param int|string $id + * + * @return PostInterface + * @throws \InvalidArgumentException + */ + public function find($id) + { + } + + /** + * @return array|PostInterface[] + */ + public function findAll() + { + $sql = new Sql($this->dbAdapter); + $select = $sql->select('posts'); + + $stmt = $sql->prepareStatementForSqlObject($select); + $result = $stmt->execute(); + + \Zend\Debug\Debug::dump($result);die(); + } + } + +Refreshing the application you should now see the following output: + +.. code-block:: text + :linenos: + + object(Zend\Db\Adapter\Driver\Pdo\Result)#303 (8) { + ["statementMode":protected] => string(7) "forward" + ["resource":protected] => object(PDOStatement)#296 (1) { + ["queryString"] => string(29) "SELECT `posts`.* FROM `posts`" + } + ["options":protected] => NULL + ["currentComplete":protected] => bool(false) + ["currentData":protected] => NULL + ["position":protected] => int(-1) + ["generatedValue":protected] => string(1) "0" + ["rowCount":protected] => NULL + } + +As you can see we do not get any data returned. Instead we are presented with a dump of some ``Result`` object that +appears to have no data in it whatsoever. But this is a faulty assumption. This ``Result`` object only has information +available for you when you actually try to access it. To make use of the data within the ``Result`` object the best +approach would be to pass the ``Result`` object over into a ``ResultSet`` object, as long as the query was successful. + +.. code-block:: php + :linenos: + :emphasize-lines: 7, 47-53 + + dbAdapter = $dbAdapter; + } + + /** + * @param int|string $id + * + * @return PostInterface + * @throws \InvalidArgumentException + */ + public function find($id) + { + } + + /** + * @return array|PostInterface[] + */ + public function findAll() + { + $sql = new Sql($this->dbAdapter); + $select = $sql->select('posts'); + + $stmt = $sql->prepareStatementForSqlObject($select); + $result = $stmt->execute(); + + if ($result instanceof ResultInterface && $result->isQueryResult()) { + $resultSet = new ResultSet(); + + \Zend\Debug\Debug::dump($resultSet->initialize($result));die(); + } + + die("no data"); + } + } + +Refreshing the page you should now see the dump of a ``ResultSet`` object that has a property +``["count":protected] => int(5)``. Meaning we have five rows inside our database. + +.. code-block:: text + :linenos: + :emphasize-lines: 12 + + object(Zend\Db\ResultSet\ResultSet)#304 (8) { + ["allowedReturnTypes":protected] => array(2) { + [0] => string(11) "arrayobject" + [1] => string(5) "array" + } + ["arrayObjectPrototype":protected] => object(ArrayObject)#305 (1) { + ["storage":"ArrayObject":private] => array(0) { + } + } + ["returnType":protected] => string(11) "arrayobject" + ["buffer":protected] => NULL + ["count":protected] => int(2) + ["dataSource":protected] => object(Zend\Db\Adapter\Driver\Pdo\Result)#303 (8) { + ["statementMode":protected] => string(7) "forward" + ["resource":protected] => object(PDOStatement)#296 (1) { + ["queryString"] => string(29) "SELECT `posts`.* FROM `posts`" + } + ["options":protected] => NULL + ["currentComplete":protected] => bool(false) + ["currentData":protected] => NULL + ["position":protected] => int(-1) + ["generatedValue":protected] => string(1) "0" + ["rowCount":protected] => int(2) + } + ["fieldCount":protected] => int(3) + ["position":protected] => int(0) + } + +Another very interesting property is ``["returnType":protected] => string(11) "arrayobject"``. This tells us that all +database entries will be returned as an ``ArrayObject``. And this is a little problem as the ``PostMapperInterface`` +requires us to return an array of ``PostInterface`` objects. Luckily there is a very simple option for us available to +make this happen. In the examples above we have used the default ``ResultSet`` object. There is also a +``HydratingResultSet`` which will hydrate the given data into a provided object. + +This means: if we tell the ``HydratingResultSet`` to use the database data to create ``Post`` objects for us, then it +will do exactly this. Let's modify our code: + +.. code-block:: php + :linenos: + :emphasize-lines: 47-53 + + dbAdapter = $dbAdapter; + } + + /** + * @param int|string $id + * + * @return PostInterface + * @throws \InvalidArgumentException + */ + public function find($id) + { + } + + /** + * @return array|PostInterface[] + */ + public function findAll() + { + $sql = new Sql($this->dbAdapter); + $select = $sql->select('posts'); + + $stmt = $sql->prepareStatementForSqlObject($select); + $result = $stmt->execute(); + + if ($result instanceof ResultInterface && $result->isQueryResult()) { + $resultSet = new HydratingResultSet(new \Zend\Stdlib\Hydrator\ClassMethods(), new \Blog\Model\Post()); + + return $resultSet->initialize($result); + } + + return array(); + } + } + +We have changed a couple of things here. Firstly instead of a normal ``ResultSet`` we are using the +``HydratingResultSet``. This Object requires two parameters, the second one being the object to hydrate into and the +first one being the ``hydrator`` that will be used. A ``hydrator``, in short, is an object that changes any sort of +data from one format to another. The InputFormat that we have is an ``ArrayObject`` but we want ``Post``-Models. The +``ClassMethods``-hydrator will take care of this using the setter- and getter functions of our ``Post``-model. + +Instead of dumping the ``$result`` variable we now directly return the initialized ``HydratingResultSet`` so we'll be +able to access the data stored within. In case we get something else returned that is not an instance of a +``ResultInterface`` we return an empty array. + +Refreshing the page you will now see all your blog posts listed on the page. Great! + + +Refactoring hidden dependencies +=============================== + +There's one little thing that we have done that's not a best-practice. We use both a Hydrator and an Object inside our + + +.. code-block:: php + :linenos: + :emphasize-lines: 10, 19, 21, 30, 31, 59-66 + + dbAdapter = $dbAdapter; + $this->hydrator = $hydrator; + $this->postPrototype = $postPrototype; + } + + /** + * @param int|string $id + * + * @return PostInterface + * @throws \InvalidArgumentException + */ + public function find($id) + { + } + + /** + * @return array|PostInterface[] + */ + public function findAll() + { + $sql = new Sql($this->dbAdapter); + $select = $sql->select('posts'); + + $stmt = $sql->prepareStatementForSqlObject($select); + $result = $stmt->execute(); + + if ($result instanceof ResultInterface && $result->isQueryResult()) { + $resultSet = new HydratingResultSet($this->hydrator, $this->postPrototype); + + return $resultSet->initialize($result); + } + + return array(); + } + } + +Now that our mapper requires more parameters we need to update the ``ZendDbSqlMapperFactory`` and inject those +parameters. + +.. code-block:: php + :linenos: + + get('Zend\Db\Adapter\Adapter'), + new ClassMethods(false), + new Post() + ); + } + } + +With this in place you can refresh the application again and you'll see your blog posts listed once again. Our Mapper +has now a really good architecture and no more hidden dependencies. + + +Finishing the mapper +==================== + +Before we jump into the next chapter let's quickly finish the mapper by writing an implementation for the ``find()`` +method. + +.. code-block:: php + :linenos: + :emphasize-lines: 46-57 + + dbAdapter = $dbAdapter; + $this->hydrator = $hydrator; + $this->postPrototype = $postPrototype; + } + + /** + * @param int|string $id + * + * @return PostInterface + * @throws \InvalidArgumentException + */ + public function find($id) + { + $sql = new Sql($this->dbAdapter); + $select = $sql->select('posts'); + $select->where(array('id = ?' => $id)); + + $stmt = $sql->prepareStatementForSqlObject($select); + $result = $stmt->execute(); + + if ($result instanceof ResultInterface && $result->isQueryResult() && $result->getAffectedRows()) { + return $this->hydrator->hydrate($result->current(), $this->postPrototype); + } + + throw new \InvalidArgumentException("Blog with given ID:{$id} not found."); + } + + /** + * @return array|PostInterface[] + */ + public function findAll() + { + $sql = new Sql($this->dbAdapter); + $select = $sql->select('posts'); + + $stmt = $sql->prepareStatementForSqlObject($select); + $result = $stmt->execute(); + + if ($result instanceof ResultInterface && $result->isQueryResult()) { + $resultSet = new HydratingResultSet($this->hydrator, $this->postPrototype); + + return $resultSet->initialize($result); + } + + return array(); + } + } + +The ``find()`` function looks really similar to the ``findAll()`` function. There's just three simple differences. +Firstly we need to add a condition to the query to only select one row. This is done using the ``where()`` function of +the ``Sql`` object. Then we also check if the ``$result`` has a row in it through ``getAffectedRows()``. The return +statement then will be hydrated using the injected hydrator into the prototype that has also been injected. + +This time, when we do not find a row we will throw an ``\InvalidArgumentException`` so that the application will easily +be able to handle the scenario. + + +Conclusion +========== + +Finishing this chapter you now know how to query for data using the ``Zend\Db\Sql`` classes. You have also learned about +the ``Zend\Stdlib\Hydrator``-Component which is one of the new key components of ZF2. Furthermore you have once again +proven that you are able to manage proper dependency injection. + +In the next chapter we'll take a closer look at the router so we'll be able to do some more action within our Module. diff --git a/docs/languages/en/in-depth-guide/zend-form-zend-form-fieldset.rst b/docs/languages/en/in-depth-guide/zend-form-zend-form-fieldset.rst new file mode 100644 index 000000000..febecff66 --- /dev/null +++ b/docs/languages/en/in-depth-guide/zend-form-zend-form-fieldset.rst @@ -0,0 +1,1116 @@ +Making use of Forms and Fieldsets +================================= + +So far all we did was read data from the database. In a real-life-application this won't get us very far as very often +the least we need to do is to support full ``Create``, ``Read``, ``Update`` and ``Delete`` operations (CRUD). Most +often the process of getting data into our database is that a user enters the data into a web ``
`` and the +application then uses the user input saves it into our backend. + +We want to be able to do exactly this and Zend Framework provides us with all the tools we need to achieve our goal. +Before we jump into coding, we need to understand the two core components for this task first. So let's take a look at +what these components are and what they are used for. + +Zend\\Form\\Fieldset +-------------------- + +The first component that you have to know about is ``Zend\Form\Fieldset``. A ``Fieldset`` is a component that contains a +reusable set of elements. You will use the ``Fieldset`` to create the frontend-input for your backend-models. It is +considered good practice to have one ``Fieldset`` for every ``Model`` of your application. + +The ``Fieldset``-component, however, is no ``Form``, meaning you will not be able to use a ``Fieldset`` without attaching it +to the ``Form``-component. The advantage here is that you have one set of elements that you can re-use for as many +``Forms`` as you like without having to re-declare all the inputs for the ``Model`` that's represented by the ``Fieldset``. + +Zend\\Form\\Form +---------------- + +The main component you'll need and that most probably you've heard about already is ``Zend\Form\Form``. The +``Form``-component is the main container for all elements of your web ````. You are able to add single +elements or a set of elements in the form of a ``Fieldset``, too. + + +Creating your first Fieldset +============================ + +Explaining how the ``Zend\Form`` component works is best done by giving you real code to work with. So let's jump right +into it and create all the forms we need to finish our ``Blog`` module. We start by creating a ``Fieldset`` that contains +all the input elements that we need to work with our ``Blog``-data. + +- You will need one hidden input for the ``id`` property, which is only needed for editting and deleting data. +- You will need one text input for the ``text`` property +- You will need one text input for the ``title`` property + +Create the file ``/module/Blog/src/Blog/Form/PostFieldset.php`` and add the following code: + +.. code-block:: php + :linenos: + :emphasize-lines: + + add(array( + 'type' => 'hidden', + 'name' => 'id' + )); + + $this->add(array( + 'type' => 'text', + 'name' => 'text', + 'options' => array( + 'label' => 'The Text' + ) + )); + + $this->add(array( + 'type' => 'text', + 'name' => 'title', + 'options' => array( + 'label' => 'Blog Title' + ) + )); + } + } + +As you can see this class is pretty handy. All we do is to have our class extend ``Zend\Form\Fieldset`` and then we +write a ``__construct()`` method and add all the elements we need to the fieldset. This ``Fieldset`` can now be used by +as many forms as we want. So let's go ahead and create our first ``Form``. + + +Creating the PostForm +===================== + +Now that we have our ``PostFieldset`` in place, we need to use it inside a ``Form``. We then need to add a Submit-Button +to the form so that the user will be able to submit the data and we're done. So create the ``PostForm`` within the +same directory under ``/module/Blog/src/Blog/Form/PostForm`` and add the ``PostFieldset`` to it: + +.. code-block:: php + :linenos: + :emphasize-lines: 12, 13 + + add(array( + 'name' => 'post-fieldset', + 'type' => 'Blog\Form\PostFieldset' + )); + + $this->add(array( + 'type' => 'submit', + 'name' => 'submit', + 'attributes' => array( + 'value' => 'Insert new Post' + ) + )); + } + } + +And that's our form. Nothing special here, we add our ``PostFieldset`` to the Form, we add a submit button to the form +and nothing more. Let's now make use of the Form. + + +Adding a new Post +================= + +Now that we have the ``PostForm`` written we want to use it. But there are a couple more tasks that you need to do. +The tasks that are standing right in front of you are: + +- create a new controller ``WriteController`` +- add ``PostService`` as a dependency to the ``WriteController`` +- add ``PostForm`` as a dependency to the ``WriteController`` +- create a new route ``blog/add`` that routes to the ``WriteController`` and its ``addAction()`` +- create a new view that displays the form + + +Creating the WriteController +---------------------------- + +As you can see from the task-list we need a new controller and this controller is supposed to have two dependencies. +One dependency being the ``PostService`` that's also being used within our ``ListController`` and the other dependency +being the ``PostForm`` which is new. Since the ``PostForm`` is a dependency that the ``ListController`` doesn't +need to display blog-data, we will create a new controller to keep things properly separated. First, register a +controller-factory within the configuration: + +.. code-block:: php + :linenos: + :emphasize-lines: 10 + + array( /** DB Config */ ), + 'service_manager' => array( /** ServiceManager Config */), + 'view_manager' => array( /** ViewManager Config */ ), + 'controllers' => array( + 'factories' => array( + 'Blog\Controller\List' => 'Blog\Factory\ListControllerFactory', + 'Blog\Controller\Write' => 'Blog\Factory\WriteControllerFactory' + ) + ), + 'router' => array( /** Router Config */ ) + ); + +Nest step would be to write the ``WriteControllerFactory``. Have the factory return the ``WriteController`` and add the +required dependencies within the constructor. + +.. code-block:: php + :linenos: + + getServiceLocator(); + $postService = $realServiceLocator->get('Blog\Service\PostServiceInterface'); + $postInsertForm = $realServiceLocator->get('FormElementManager')->get('Blog\Form\PostForm'); + + return new WriteController( + $postService, + $postInsertForm + ); + } + } + +In this code-example there are a couple of things to be aware of. First, the ``WriteController`` doesn't exist yet, but we +will create this in the next step so we're just assuming that it will exist later on. Second, we access the +``FormElementManager`` to get access to our ``PostForm``. All forms should be accessed through the ``FormElementManager``. +Even though we haven't registered the ``PostForm`` in our config files yet the ``FormElementManager`` automatically knows +about forms that act as ``invokables``. As long as you have no dependencies you don't need to register them explicitly. + +Next up is the creation of our controller. Be sure to type hint the dependencies by their interfaces and to add the +``addAction()``! + +.. code-block:: php + :linenos: + + postService = $postService; + $this->postForm = $postForm; + } + + public function addAction() + { + } + } + +Right on to creating the new route: + +.. code-block:: php + :linenos: + :emphasize-lines: 33-42 + + array( /** Db Config */ ), + 'service_manager' => array( /** ServiceManager Config */ ), + 'view_manager' => array( /** ViewManager Config */ ), + 'controllers' => array( /** Controller Config */ ), + 'router' => array( + 'routes' => array( + 'blog' => array( + 'type' => 'literal', + 'options' => array( + 'route' => '/blog', + 'defaults' => array( + 'controller' => 'Blog\Controller\List', + 'action' => 'index', + ) + ), + 'may_terminate' => true, + 'child_routes' => array( + 'detail' => array( + 'type' => 'segment', + 'options' => array( + 'route' => '/:id', + 'defaults' => array( + 'action' => 'detail' + ), + 'constraints' => array( + 'id' => '\d+' + ) + ) + ), + 'add' => array( + 'type' => 'literal', + 'options' => array( + 'route' => '/add', + 'defaults' => array( + 'controller' => 'Blog\Controller\Write', + 'action' => 'add' + ) + ) + ) + ) + ) + ) + ) + ); + +And lastly let's create a dummy template: + +.. code-block:: html + :linenos: + + +

WriteController::addAction()

+ +**Checking the current status** + +If you try to access the new route ``localhost:8080/blog/add`` you're supposed to see the following error message: + +.. code-block:: text + :linenos: + + Fatal error: Call to a member function insert() on a non-object in + {libraryPath}/Zend/Form/Fieldset.php on line {lineNumber} + +If this is not the case, be sure to follow the tutorial correctly and carefully check all your files. Assuming you are +getting this error, let's find out what it means and fix it! + + +The above error message is very common and its solution isn't that intuitive. It appears that there is an error within +the ``Zend/Form/Fieldset.php`` but that's not the case. The error message let's you know that something didn't go right +while you were creating your form. In fact, while creating both the ``PostForm`` as well as the ``PostFieldset`` we +have forgotten something very, very important. + +.. note:: + + When overwriting a ``__construct()`` method within the ``Zend\Form``-component, be sure to always call + ``parent::__construct()``! + +Without this, forms and fieldsets will not be able to get initiated correctly. Let's now fix +the problem by calling the parents constructor in both form and fieldset. To have more flexibility we will also +include the signature of the ``__construct()`` function which accepts a couple of parameters. + +.. code-block:: php + :linenos: + :emphasize-lines: 9, 11 + + add(array( + 'name' => 'post-fieldset', + 'type' => 'Blog\Form\PostFieldset' + )); + + $this->add(array( + 'type' => 'submit', + 'name' => 'submit', + 'attributes' => array( + 'value' => 'Insert new Post' + ) + )); + } + } + +As you can see our ``PostForm`` now accepts two parameters to give our form a name and to set a couple of options. Both +parameters will be passed along to the parent. If you look closely at how we add the ``PostFieldset`` to the form you'll +notice that we assign a name to the fieldset. Those options will be passed from the ``FormElementManager`` when the +``PostFieldset`` is created. But for this to function we need to do the same step inside our fieldset, too: + +.. code-block:: php + :linenos: + :emphasize-lines: 9, 11 + + add(array( + 'type' => 'hidden', + 'name' => 'id' + )); + + $this->add(array( + 'type' => 'text', + 'name' => 'text', + 'options' => array( + 'label' => 'The Text' + ) + )); + + $this->add(array( + 'type' => 'text', + 'name' => 'title', + 'options' => array( + 'label' => 'Blog Title' + ) + )); + } + } + +Reloading your application now will yield you the desired result. + + +Displaying the form +=================== + +Now that we have our ``PostForm`` within our ``WriteController`` it's time to pass this form to the view and have +it rendered using the provided ``ViewHelpers`` from the ``Zend\Form`` component. First change your controller so that the +form is passed to the view. + +.. code-block:: php + :linenos: + :emphasize-lines: 8, 26-28 + + postService = $postService; + $this->postForm = $postForm; + } + + public function addAction() + { + return new ViewModel(array( + 'form' => $this->postForm + )); + } + } + +And then we need to modify our view to have the form rendered. + + +.. code-block:: php + :linenos: + :emphasize-lines: 3-13 + + +

WriteController::addAction()

+ form; + $form->setAttribute('action', $this->url()); + $form->prepare(); + + echo $this->form()->openTag($form); + + echo $this->formCollection($form); + + echo $this->form()->closeTag(); + +Firstly, we tell the form that it should send its data to the current URL and then we tell the form to ``prepare()`` +itself which triggers a couple of internal things. + +.. note:: + + HTML-Forms can be sent using ``POST`` and ``GET``. ZF2s default is ``POST``, therefore you don't have to be + explicit in setting this options. If you want to change it to ``GET`` though, all you have to do is to set the + specific attribute prior to the ``prepare()`` call. + + ``$form->setAttribute('method', 'GET');`` + +Next we're using a couple of ``ViewHelpers`` which take care of rendering the form for us. There are many different ways +to render a form within Zend Framework but using ``formCollection()`` is probably the fastest one. + +Refreshing the browser you will now see your form properly displayed. However, if we're submitting the form all we see +is our form being displayed again. And this is due to the simple fact that we didn't add any logic to the controller +yet. + +.. note:: + + Keep in mind that this tutorial focuses solely on the OOP aspect of things. Rendering the form like this, without + any stylesheets added doesn't really reflect most designers' idea of a beautiful form. You'll find out more about + the rendering of forms in the chapter of :ref:`Zend\\Form\\View\\Helper `. + + +Controller Logic for basically all Forms +======================================== + +Writing a Controller that handles a form workflow is pretty simple and it's basically identical for each and every +form you have within your application. + +1. You want to check if the current request is a POST-Request, meaning if the form has been sent +2. If the form has been sent, you want to: + - store the POST-Data within the Form + - check if the form passes validation +3. If the form passes validation, you want to: + - pass the form data to your service to have it stored + - redirect the user to either the detail page of the entered data or to some overview page +4. In all other cases, you want the form displayed, sometimes alongside given error messages + +And all of this is really not that much code. Modify your ``WriteController`` to the following code: + +.. code-block:: php + :linenos: + :emphasize-lines: 26-40 + + postService = $postService; + $this->postForm = $postForm; + } + + public function addAction() + { + $request = $this->getRequest(); + + if ($request->isPost()) { + $this->postForm->setData($request->getPost()); + + if ($this->postForm->isValid()) { + try { + $this->postService->savePost($this->postForm->getData()); + + return $this->redirect()->toRoute('post'); + } catch (\Exception $e) { + // Some DB Error happened, log it and let the user know + } + } + } + + return new ViewModel(array( + 'form' => $this->postForm + )); + } + } + +This example code should be pretty straight forward. First we save the current request into a local variable. Then we +check if the current request ist a POST-Request and if so, we store the requests POST-data into the form. If the form +turns out to be valid we try to save the form data through our service and then redirect the user to the route ``blog``. +If any error occurred at any point we simply display the form again. + +Submitting the form right now will return into the following error + +.. code-block:: text + :linenos: + + Fatal error: Call to undefined method Blog\Service\PostService::savePost() in + /module/Blog/src/Blog/Controller/WriteController.php on line 33 + +Let's fix this by extending our ``PostService``. Be sure to also change the signature of the ``PostServiceInterface``! + +.. code-block:: php + :linenos: + :emphasize-lines: 32 + + postMapper = $postMapper; + } + + /** + * {@inheritDoc} + */ + public function findAllPosts() + { + return $this->postMapper->findAll(); + } + + /** + * {@inheritDoc} + */ + public function findPost($id) + { + return $this->postMapper->find($id); + } + + /** + * {@inheritDoc} + */ + public function savePost(PostInterface $post) + { + return $this->postMapper->save($post); + } + } + +And now that we're making an assumption against our ``postMapper`` we need to extend the ``PostMapperInterface`` and its +implementation, too. Start by extending the interface: + +.. code-block:: php + :linenos: + :emphasize-lines: 28 + + dbAdapter = $dbAdapter; + $this->hydrator = $hydrator; + $this->postPrototype = $postPrototype; + } + + /** + * @param int|string $id + * + * @return PostInterface + * @throws \InvalidArgumentException + */ + public function find($id) + { + $sql = new Sql($this->dbAdapter); + $select = $sql->select('posts'); + $select->where(array('id = ?' => $id)); + + $stmt = $sql->prepareStatementForSqlObject($select); + $result = $stmt->execute(); + + if ($result instanceof ResultInterface && $result->isQueryResult() && $result->getAffectedRows()) { + return $this->hydrator->hydrate($result->current(), $this->postPrototype); + } + + throw new \InvalidArgumentException("Blog with given ID:{$id} not found."); + } + + /** + * @return array|PostInterface[] + */ + public function findAll() + { + $sql = new Sql($this->dbAdapter); + $select = $sql->select('posts'); + + $stmt = $sql->prepareStatementForSqlObject($select); + $result = $stmt->execute(); + + if ($result instanceof ResultInterface && $result->isQueryResult()) { + $resultSet = new HydratingResultSet($this->hydrator, $this->postPrototype); + + return $resultSet->initialize($result); + } + + return array(); + } + + /** + * @param PostInterface $postObject + * + * @return PostInterface + * @throws \Exception + */ + public function save(PostInterface $postObject) + { + $postData = $this->hydrator->extract($postObject); + unset($postData['id']); // Neither Insert nor Update needs the ID in the array + + if ($postObject->getId()) { + // ID present, it's an Update + $action = new Update('post'); + $action->set($postData); + $action->where(array('id = ?' => $postObject->getId())); + } else { + // ID NOT present, it's an Insert + $action = new Insert('post'); + $action->values($postData); + } + + $sql = new Sql($this->dbAdapter); + $stmt = $sql->prepareStatementForSqlObject($action); + $result = $stmt->execute(); + + if ($result instanceof ResultInterface) { + if ($newId = $result->getGeneratedValue()) { + // When a value has been generated, set it on the object + $postObject->setId($newId); + } + + return $postObject; + } + + throw new \Exception("Database error"); + } + } + +The ``save()`` function handles two cases. The ``insert`` and ``update`` routine. Firstly we extract the ``Post``-Object +since we need array data to work with ``Insert`` and ``Update``. Then we remove the ``id`` from the array since this +field is not wanted. When we do an update of a row, we don't update the ``id`` property itself and therefore it isn't +needed. On the insert routine we don't need an ``id`` either so we can simply strip it away. + +After the ``id`` field has been removed we check what action is supposed to be called. If the ``Post``-Object has an ``id`` +set we create a new ``Update``-Object and if not we create a new ``Insert``-Object. We set the data for both actions +accordingly and after that the data is passed over to the ``Sql``-Object for the actual query into the database. + +At last we check if we receive a valid result and if there has been an ``id`` generated. If it's the case we call the +``setId()``-function of our blog and return the object in the end. + +Let's submit our form again and see what we get. + +.. code-block:: text + :linenos: + + Catchable fatal error: Argument 1 passed to Blog\Service\PostService::savePost() + must implement interface Blog\Model\PostInterface, array given, + called in /module/Blog/src/Blog/Controller/InsertController.php on line 33 + and defined in /module/Blog/src/Blog/Service/PostService.php on line 49 + +Forms, per default, give you data in an array format. But our ``PostService`` expects the format to be an implementation +of the ``PostInterface``. This means we need to find a way to have this array data become object data. If you recall the +previous chapter, this is done through the use of hydrators. + +.. note:: + + On the Update-Query you'll notice that we have assigned a condition to only update the row matching a given id + + ``$action->where(array('id = ?' => $postObject->getId()));`` + + You'll see here that the condition is: **id equals ?**. With the question-mark being the id of the post-object. In + the same way you could assign a condition to update (or select) rows with all entries higher than a given id: + + ``$action->where(array('id > ?' => $postObject->getId()));`` + + This works for all conditions. ``=``, ``>``, ``<``, ``>=`` and ``<=`` + + +Zend\\Form and Zend\\Stdlib\\Hydrator working together +====================================================== + +Before we go ahead and put the hydrator into the form, let's first do a data-dump of the data coming from the form. That +way we can easily notice all changes that the hydrator does. Modify your ``WriteController`` to the following: + +.. code-block:: php + :linenos: + :emphasize-lines: 33 + + postService = $postService; + $this->postForm = $postForm; + } + + public function addAction() + { + $request = $this->getRequest(); + + if ($request->isPost()) { + $this->postForm->setData($request->getPost()); + + if ($this->postForm->isValid()) { + try { + \Zend\Debug\Debug::dump($this->postForm->getData());die(); + $this->postService->savePost($this->postForm->getData()); + + return $this->redirect()->toRoute('post'); + } catch (\Exception $e) { + // Some DB Error happened, log it and let the user know + } + } + } + + return new ViewModel(array( + 'form' => $this->postForm + )); + } + } + +With this set up go ahead and submit the form once again. You should now see a data dump like the following: + +.. code-block:: text + :linenos: + + array(2) { + ["submit"] => string(16) "Insert new Post" + ["post-fieldset"] => array(3) { + ["id"] => string(0) "" + ["text"] => string(3) "foo" + ["title"] => string(3) "bar" + } + } + +Now telling your fieldset to hydrate its data into an ``Post``-object is very simple. All you need to do is to assign +the hydrator and the object prototype like this: + +.. code-block:: php + :linenos: + :emphasize-lines: 5, 7, 15, 16 + + setHydrator(new ClassMethods(false)); + $this->setObject(new Post()); + + $this->add(array( + 'type' => 'hidden', + 'name' => 'id' + )); + + $this->add(array( + 'type' => 'text', + 'name' => 'text', + 'options' => array( + 'label' => 'The Text' + ) + )); + + $this->add(array( + 'type' => 'text', + 'name' => 'title', + 'options' => array( + 'label' => 'Blog Title' + ) + )); + } + } + +As you can see we're doing two things. We tell the fieldset to be using the ``ClassMethods`` hydrator and then we tell the +fieldset that the default object to be returned is our ``Blog``-Model. However, when you're re-submitting the form now +you'll notice that nothing has changed. We're still only getting array data returned and no object. + +This is due to the fact that the form itself doesn't know that it has to return an object. When the form doesn't know +that it's supposed to return an object it uses the ``ArraySeriazable`` hydrator recursively. To change this, all we need +to do is to make our ``PostFieldset`` a so-called ``base_fieldset``. + +A ``base_fieldset`` basically tells the form "this form is all about me, don't worry about other data, just worry about +me". And when the form knows that this fieldset is the real deal, then the form will use the hydrator presented by the +fieldset and return the object that we desire. Modify your ``PostForm`` and assign the ``PostFieldset`` as +``base_fieldset``: + +.. code-block:: php + :linenos: + :emphasize-lines: 16-18 + + add(array( + 'name' => 'post-fieldset', + 'type' => 'Blog\Form\PostFieldset', + 'options' => array( + 'use_as_base_fieldset' => true + ) + )); + + $this->add(array( + 'type' => 'submit', + 'name' => 'submit', + 'attributes' => array( + 'value' => 'Insert new Post' + ) + )); + } + } + +Now submit your form again. You should see the following output: + +.. code-block:: text + :linenos: + + object(Blog\Model\Post)#294 (3) { + ["id":protected] => string(0) "" + ["title":protected] => string(3) "foo" + ["text":protected] => string(3) "bar" + } + +You can now revert back your ``WriteController`` to its previous form to have the form-data passed through the +``PostService``. + +.. code-block:: php + :linenos: + :emphasize-lines: 33 + + postService = $postService; + $this->postForm = $postForm; + } + + public function addAction() + { + $request = $this->getRequest(); + + if ($request->isPost()) { + $this->postForm->setData($request->getPost()); + + if ($this->postForm->isValid()) { + try { + $this->postService->savePost($this->postForm->getData()); + + return $this->redirect()->toRoute('post'); + } catch (\Exception $e) { + // Some DB Error happened, log it and let the user know + } + } + } + + return new ViewModel(array( + 'form' => $this->postForm + )); + } + } + +If you send the form now you'll now be able to add as many new blogs as you want. Great! + + +Conclusion +========== + +In this chapter you've learned a great deal about the ``Zend\Form`` component. You've learned that ``Zend\Stdlib\Hydrator`` +takes a big part within the ``Zend\Form`` component and by making use of both components you've been able to create an +insert form for the blog module. + +In the next chapter we will finalize the CRUD functionality by creating the update and delete routines for the blog +module. diff --git a/docs/languages/en/index.rst b/docs/languages/en/index.rst index fbd7e1bfb..c15c30311 100644 --- a/docs/languages/en/index.rst +++ b/docs/languages/en/index.rst @@ -339,6 +339,20 @@ * :doc:`user-guide/forms-and-actions` * :doc:`user-guide/conclusion` +|InDepthTutorial| +----------------- + +|InDepthTutorialIntroduction| + + * :doc:`in-depth-guide/first-module` + * :doc:`in-depth-guide/services-and-servicemanager` + * :doc:`in-depth-guide/preparing-db-backend` + * :doc:`in-depth-guide/zend-db-sql-zend-stdlib-hydrator` + * :doc:`in-depth-guide/understanding-routing` + * :doc:`in-depth-guide/zend-form-zend-form-fieldset` + * :doc:`in-depth-guide/data-binding` + * :doc:`in-depth-guide/review` + |GettingStartedWithZendStudio| ------------------------------ diff --git a/docs/languages/en/modules/zend.config.reader.rst b/docs/languages/en/modules/zend.config.reader.rst index 65d140589..d63bff22d 100644 --- a/docs/languages/en/modules/zend.config.reader.rst +++ b/docs/languages/en/modules/zend.config.reader.rst @@ -74,7 +74,7 @@ We can use the ``Zend\Config\Reader\Ini`` to read this INI file: $reader = new Zend\Config\Reader\Ini(); $data = $reader->fromFile('/path/to/config.ini'); - echo $data['webhost'] // prints "www.example.com" + echo $data['webhost']; // prints "www.example.com" echo $data['database']['params']['dbname']; // prints "dbproduction" The ``Zend\Config\Reader\Ini`` supports a feature to include the content of a INI file in a specific section of @@ -135,7 +135,7 @@ The following example illustrates a basic use of ``Zend\Config\Reader\Xml`` for .. code-block:: xml :linenos: - ?> + www.example.com @@ -157,8 +157,8 @@ We can use the ``Zend\Config\Reader\Xml`` to read this XML file: $reader = new Zend\Config\Reader\Xml(); $data = $reader->fromFile('/path/to/config.xml'); - echo $data['webhost'] // prints "www.example.com" - echo $data['database']['params']['dbname']; // prints "dbproduction" + echo $data['webhost']; // prints "www.example.com" + echo $data['database']['params']['dbname']['value']; // prints "dbproduction" ``Zend\Config\Reader\Xml`` utilizes the `XMLReader`_ *PHP* class. Please review this documentation to be aware of its specific behaviors, which propagate to ``Zend\Config\Reader\Xml``. @@ -232,7 +232,7 @@ We can use the ``Zend\Config\Reader\Json`` to read this JSON file: $reader = new Zend\Config\Reader\Json(); $data = $reader->fromFile('/path/to/config.json'); - echo $data['webhost'] // prints "www.example.com" + echo $data['webhost']; // prints "www.example.com" echo $data['database']['params']['dbname']; // prints "dbproduction" ``Zend\Config\Reader\Json`` utilizes the :ref:`Zend\\Json\\Json ` class. @@ -298,7 +298,7 @@ We can use the ``Zend\Config\Reader\Yaml`` to read this YAML file: $reader = new Zend\Config\Reader\Yaml(); $data = $reader->fromFile('/path/to/config.yaml'); - echo $data['webhost'] // prints "www.example.com" + echo $data['webhost']; // prints "www.example.com" echo $data['database']['params']['dbname']; // prints "dbproduction" If you want to use an external YAML reader you have to pass the callback function in the constructor of the class. @@ -313,7 +313,7 @@ For instance, if you want to use the `Spyc`_ library: $reader = new Zend\Config\Reader\Yaml(array('Spyc','YAMLLoadString')); $data = $reader->fromFile('/path/to/config.yaml'); - echo $data['webhost'] // prints "www.example.com" + echo $data['webhost']; // prints "www.example.com" echo $data['database']['params']['dbname']; // prints "dbproduction" You can also instantiate the ``Zend\Config\Reader\Yaml`` without any parameter and specify the YAML reader in a diff --git a/docs/languages/en/modules/zend.filter.inflector.rst b/docs/languages/en/modules/zend.filter.inflector.rst index 7230621b1..09cf3fb36 100644 --- a/docs/languages/en/modules/zend.filter.inflector.rst +++ b/docs/languages/en/modules/zend.filter.inflector.rst @@ -279,8 +279,8 @@ filter rules, according to the following notation: // Could also use setRules() with this notation: $inflector->addRules(array( // filter rules: - ':controller' => array('CamelCaseToUnderscore','StringToLower'), - ':action' => array('CamelCaseToUnderscore','StringToLower'), + ':controller' => array('Word\CamelCaseToUnderscore','StringToLower'), + ':action' => array('Word\CamelCaseToUnderscore','StringToLower'), // Static rule: 'suffix' => 'phtml' diff --git a/docs/languages/en/modules/zend.filter.upper-case-words.rst b/docs/languages/en/modules/zend.filter.upper-case-words.rst new file mode 100644 index 000000000..4534d8367 --- /dev/null +++ b/docs/languages/en/modules/zend.filter.upper-case-words.rst @@ -0,0 +1,50 @@ +:orphan: + +.. _zend.filter.set.uppercasewords: + +UpperCaseWords +------------- + +This filter converts any input to uppercase the first character of each word. + +.. _zend.filter.set.uppercasewords.options: + +Supported Options +^^^^^^^^^^^^^^^^^ + +The following options are supported for ``Zend\Filter\UpperCaseWords``: + +- **encoding**: This option can be used to set an encoding which has to be used. + +.. _zend.filter.set.uppercasewords.basic: + +Basic Usage +^^^^^^^^^^^ + +This is a basic example for using the ``UpperCaseWords`` filter: + +.. code-block:: php + :linenos: + + $filter = new Zend\Filter\UpperCaseWords(); + + print $filter->filter('sample of title'); + // returns "Sample Of Title" + +.. _zend.filter.set.uppercasewords.encoding: + +Different Encoded Strings +^^^^^^^^^^^^^^^^^^^^^^^^^ + +Like the ``StringToLower`` filter, this filter handles only characters from the actual locale of your server. Using +different character sets works the same as with ``StringToLower``. + +.. code-block:: php + :linenos: + + $filter = new Zend\Filter\UpperCaseWords(array('encoding' => 'UTF-8')); + + // or do this afterwards + $filter->setEncoding('ISO-8859-1'); + + diff --git a/docs/languages/en/modules/zend.http.client.advanced.rst b/docs/languages/en/modules/zend.http.client.advanced.rst index fd0dcc1eb..98fe1dac2 100644 --- a/docs/languages/en/modules/zend.http.client.advanced.rst +++ b/docs/languages/en/modules/zend.http.client.advanced.rst @@ -55,19 +55,15 @@ modification is required. Cookies can be added using either the `addCookie()` or // Easy and simple: by providing a cookie name and cookie value $client->addCookie('flavor', 'chocolate chips'); - // By directly providing a raw cookie string (name=value) - // Note that the value must be already URL encoded - $client->addCookie('flavor=chocolate%20chips'); - // By providing a Zend\Http\Header\SetCookie object - $cookie = Zend\Http\Header\SetCookie::fromString('flavor=chocolate%20chips'); + $cookie = Zend\Http\Header\SetCookie::fromString('Set-Cookie: flavor=chocolate%20chips'); $client->addCookie($cookie); // Multiple cookies can be set at once by providing an // array of Zend\Http\Header\SetCookie objects $cookies = array( - Zend\Http\Header\SetCookie::fromString('flavorOne=chocolate%20chips'), - Zend\Http\Header\SetCookie::fromString('flavorTwo=vanilla'), + Zend\Http\Header\SetCookie::fromString('Set-Cookie: flavorOne=chocolate%20chips'), + Zend\Http\Header\SetCookie::fromString('Set-Cookie: flavorTwo=vanilla'), ); $client->addCookie($cookies); @@ -82,17 +78,10 @@ adding the new cookies: .. code-block:: php :linenos: - // setCookies accepts an array of cookie values, which - // can be in either of the following formats: + // setCookies accepts an array of cookie values as $name => $value $client->setCookies(array( - - // A raw cookie string (name=value) - // Note that the value must be already URL encoded - 'flavor=chocolate%20chips', - - // A Zend\Http\Header\SetCookie object - Zend\Http\Header\SetCookie::fromString('flavor=chocolate%20chips'), - + 'flavor' => 'chocolate chips', + 'amount' => 10, )); @@ -115,13 +104,15 @@ cookie before sending further requests. // First request: log in and start a session $client->setUri('http://example.com/login.php'); $client->setParameterPost(array('user' => 'h4x0r', 'password' => 'l33t')); - $response = $client->request('POST'); + $client->setMethod('POST'); + + $response = $client->getResponse(); $cookies->addCookiesFromResponse($response, $client->getUri()); // Now we can send our next request $client->setUri('http://example.com/read_member_news.php'); - $client->addCookies($cookies->getMatchingCookies($client->getUri()); - $client->request('GET'); + $client->setCookies($cookies->getMatchingCookies($client->getUri())); + $client->setMethod('GET'); For more information about the ``Zend\Http\Cookies`` class, see :ref:`this section `. @@ -185,9 +176,9 @@ will be erased. // Setting multiple headers. Will remove all existing // headers and add new ones to the Request header container $client->setHeaders(array( - Zend\Http\Header\Host::fromString('Host: www.example.com'), - 'Accept-encoding' => 'gzip,deflate', - 'X-Powered-By: Zend Framework' + Zend\Http\Header\Host::fromString('Host: www.example.com'), + 'Accept-Encoding' => 'gzip,deflate', + 'X-Powered-By: Zend Framework', )); diff --git a/docs/languages/en/modules/zend.i18n.translating.rst b/docs/languages/en/modules/zend.i18n.translating.rst index 90fff34e4..d643c6685 100644 --- a/docs/languages/en/modules/zend.i18n.translating.rst +++ b/docs/languages/en/modules/zend.i18n.translating.rst @@ -29,7 +29,7 @@ To add a single file to the translator, use the ``addTranslationFile()`` method: $translator = new Translator(); $translator->addTranslationFile($type, $filename, $textDomain, $locale); -The type given there is a name of one of the format loaders listed in the next section. Filename points to the +The type given there is a name of one of the format loaders listed in the next section. Filename points to the file containing the translations, and the text domain specifies a category name for the translations. If the text domain is omitted, it will default to the "default" value. The locale specifies which language the translated strings are from and is only required for formats which contain translations for a single locale. @@ -49,7 +49,7 @@ translations to the file system, without touching your code. Patterns are added use Zend\I18n\Translator\Translator; $translator = new Translator(); - $translator->addTranslationFilePattern($type, $pattern, $textDomain); + $translator->addTranslationFilePattern($type, $baseDir, $pattern, $textDomain); The parameters for adding patterns is pretty similar to adding individual files, except that you don't specify a locale and give the file location as a sprintf pattern. The locale is passed to the sprintf call, so you can either use %s diff --git a/docs/languages/en/modules/zend.i18n.view.helper.currency.format.rst b/docs/languages/en/modules/zend.i18n.view.helper.currency.format.rst index d47c0aef1..ecec2c139 100644 --- a/docs/languages/en/modules/zend.i18n.view.helper.currency.format.rst +++ b/docs/languages/en/modules/zend.i18n.view.helper.currency.format.rst @@ -24,16 +24,16 @@ Basic Usage echo $this->currencyFormat(1234.56, 'EUR', null, 'de_DE'); // This returns: "1.234,56 €" - echo $this->currencyFormat(1234.56, null, true); + echo $this->currencyFormat(1234.56, 'USD', true, 'en_US'); // This returns: "$1,234.56" - echo $this->currencyFormat(1234.56, null, false); + echo $this->currencyFormat(1234.56, 'USD', false, 'en_US'); // This returns: "$1,235" - echo $helper(12345678.90, 'EUR', true, 'de_DE', '#0.# kg'); + echo $this->currencyFormat(12345678.90, 'EUR', true, 'de_DE', '#0.# kg'); // This returns: "12345678,90 kg" - echo $helper(12345678.90, 'EUR', false, 'de_DE', '#0.# kg'); + echo $this->currencyFormat(12345678.90, 'EUR', false, 'de_DE', '#0.# kg'); // This returns: "12345679 kg" .. function:: currencyFormat(float $number [, string $currencyCode = null [, bool $showDecimals = null [, string $locale = null [, string $pattern = null ]]]]) @@ -107,7 +107,7 @@ helper is used: $this->plugin('currencyformat')->setShouldShowDecimals(false); - echo $this->currencyFormat(1234.56); + echo $this->currencyFormat(1234.56, 'USD', null, 'en_US'); // This returns: "$1,235" .. function:: setShouldShowDecimals(bool $showDecimals) diff --git a/docs/languages/en/modules/zend.mvc.plugins.rst b/docs/languages/en/modules/zend.mvc.plugins.rst index 14ae4186e..8b316ad4d 100644 --- a/docs/languages/en/modules/zend.mvc.plugins.rst +++ b/docs/languages/en/modules/zend.mvc.plugins.rst @@ -630,6 +630,14 @@ The ``Redirect`` plugin does this work for you. It offers three methods: :rtype: ``Zend\Http\Response`` +.. function:: toReferer(string $defaultUrl = null) + :noindex: + + Redirects to the referring URL if it is present in the request headers. If is not present, + redirect to the provided ``$defaultUrl``. If none of the previous two URLs are present, redirect to the base route. + + :rtype: ``Zend\Http\Response`` + In each case, the ``Response`` object is returned. If you return this immediately, you can effectively short-circuit execution of the request. diff --git a/docs/languages/en/modules/zend.mvc.quick-start.rst b/docs/languages/en/modules/zend.mvc.quick-start.rst index 31fe81fb2..3656d5b29 100644 --- a/docs/languages/en/modules/zend.mvc.quick-start.rst +++ b/docs/languages/en/modules/zend.mvc.quick-start.rst @@ -251,6 +251,37 @@ That's it. Save the file. .. _zend.mvc.quick-start.create-a-route: +View scripts for module names with subnamespaces +------------------------------------------------ + +As per PSR-0, module should be named following this rule: ``\\(\)*`` +Default controller class to template mapping does not work very well with those: it will remove subnamespace. + +To address that issue new mapping was introduced since 2.3.0. To maintain backwards compatibility that +mapping is not enabled by default. To enable it, you need to add your module namespace to whitelist in your module config: + +.. code-block:: php + :linenos: + + 'view_manager' => array( + // Controller namespace to template map + // or whitelisting for controller FQCN to template mapping + 'controller_map' => array( + '' => true, + ), + ), + +Now, create the directory ``view///hello``. Inside that directory, create a file named ``world.phtml``. +Inside that, paste in the following: + +.. code-block:: php + :linenos: + +

Greetings!

+ +

You said "escapeHtml($message) ?>".

+ + Create a Route -------------- diff --git a/docs/languages/en/modules/zend.mvc.routing.rst b/docs/languages/en/modules/zend.mvc.routing.rst index 20844f561..124b049a6 100644 --- a/docs/languages/en/modules/zend.mvc.routing.rst +++ b/docs/languages/en/modules/zend.mvc.routing.rst @@ -383,7 +383,7 @@ You may use any route type as a child route of a ``Part`` route. 'query' => 'Zend\Mvc\Router\Http\Query', 'method' => 'Zend\Mvc\Router\Http\Method', ); - $foreach ($plugins as $name => $class) { + foreach ($plugins as $name => $class) { $routePlugins->setInvokableClass($name, $class); } @@ -486,7 +486,17 @@ As a complex example: Zend\\Mvc\\Router\\Http\\Query (Deprecated) ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ -This route part is deprecated since you can now add query parameters without a query route. +.. warning:: + + **Potential security issue** + + A misuse of this route part can lead to a potential security issue. + +.. note:: + + **Deprecated** + + This route part is deprecated since you can now add query parameters without a query route. The ``Query`` route part allows you to specify and capture query string parameters for a given route. @@ -544,6 +554,25 @@ parameters of "format" and "limit" will then be appended as a query string. The output from our example should then be "/page/my-test-page?format=rss&limit=10" +.. _zend.mvc.routing.http-route-types.wildcard: + +Zend\\Mvc\\Router\\Http\\Wildcard (Deprecated) +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +.. warning:: + + **Potential security issue** + + A misuse of this route type can lead to a potential security issue. + +.. note:: + + **Deprecated** + + This route type is deprecated. Use the ``Segment`` route type. + +The ``Wildcard`` route type matches all segments of a URI path, like in version 1 of Zend Framework. + .. _zend.mvc.routing.http-route-types.examples: HTTP Routing Examples diff --git a/docs/languages/en/modules/zend.mvc.services.rst b/docs/languages/en/modules/zend.mvc.services.rst index 7c33ef064..57e416c41 100644 --- a/docs/languages/en/modules/zend.mvc.services.rst +++ b/docs/languages/en/modules/zend.mvc.services.rst @@ -754,6 +754,11 @@ local configuration file overrides the global configuration. // Default suffix to use when resolving template scripts, if none, 'phtml' is used 'default_template_suffix' => $templateSuffix, // e.g. 'php' + // Controller namespace to template map + // or whitelisting for controller FQCN to template mapping + 'controller_map' => array( + ), + // Layout template name 'layout' => $layoutTemplateName, // e.g. 'layout/layout' diff --git a/docs/languages/en/modules/zend.navigation.view.helper.menu.rst b/docs/languages/en/modules/zend.navigation.view.helper.menu.rst index c5b65e06e..0e8edee47 100644 --- a/docs/languages/en/modules/zend.navigation.view.helper.menu.rst +++ b/docs/languages/en/modules/zend.navigation.view.helper.menu.rst @@ -577,4 +577,22 @@ Output: Products Company Community - \ No newline at end of file + + +.. _zend.navigation.view.helper.menu.partial.using-acl: + +Using ACL in partial view script +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +If you want to use ACL within your partial view script, then you will have to check the access to a page manually. + +In *module/MyModule/view/my-module/partials/menu.phtml*: + +.. code-block:: php + :linenos: + + foreach ($this->container as $page) { + if ($this->navigation()->accept($page)) { + echo $this->navigation()->menu()->htmlify($page) . PHP_EOL; + } + } \ No newline at end of file diff --git a/docs/languages/en/modules/zend.session.manager.rst b/docs/languages/en/modules/zend.session.manager.rst index a36752502..3b5b7da67 100644 --- a/docs/languages/en/modules/zend.session.manager.rst +++ b/docs/languages/en/modules/zend.session.manager.rst @@ -15,7 +15,7 @@ Initializing the Session Manager -------------------------------- Generally speaking you will always want to initialize the session manager and ensure that you had initialized it -on your end; this puts in place a simple solution to prevent against session fixation. Generally you will +on your end; this puts in place a simple solution to prevent against session fixation. Generally you will setup configuration and then inside of your Application module bootstrap the session manager. Additionally you will likely want to supply validators to prevent against session hijacking. @@ -45,7 +45,7 @@ The following illustrates how you might utilize the above configuration to creat .. code-block:: php :linenos: - + use Zend\Session\SessionManager; use Zend\Session\Container; @@ -54,14 +54,13 @@ The following illustrates how you might utilize the above configuration to creat public function onBootstrap($e) { $eventManager = $e->getApplication()->getEventManager(); - $serviceManager = $e->getApplication()->getServiceManager(); $moduleRouteListener = new ModuleRouteListener(); $moduleRouteListener->attach($eventManager); $this->bootstrapSession($e); } public function bootstrapSession($e) - { + { $session = $e->getApplication() ->getServiceManager() ->get('Zend\Session\SessionManager'); @@ -69,10 +68,40 @@ The following illustrates how you might utilize the above configuration to creat $container = new Container('initialized'); if (!isset($container->init)) { - $session->regenerateId(true); - $container->init = 1; + $serviceManager = $e->getApplication()->getServiceManager(); + $request = $serviceManager->get('Request'); + + $session->regenerateId(true); + $container->init = 1; + $container->remoteAddr = $request->getServer()->get('REMOTE_ADDR'); + $container->httpUserAgent = $request->getServer()->get('HTTP_USER_AGENT'); + + $config = $serviceManager->get('Config'); + if (!isset($config['session'])) { + return; + } + + $sessionConfig = $config['session']; + if (isset($sessionConfig['validators'])) { + $chain = $session->getValidatorChain(); + + foreach ($sessionConfig['validators'] as $validator) { + switch ($validator) { + case 'Zend\Session\Validator\HttpUserAgent': + $validator = new $validator($container->httpUserAgent); + break; + case 'Zend\Session\Validator\RemoteAddr': + $validator = new $validator($container->remoteAddr); + break; + default: + $validator = new $validator(); + } + + $chain->attach('session.validate', array($validator, 'isValid')); + } + } } - } + } public function getServiceConfig() { @@ -104,15 +133,6 @@ The following illustrates how you might utilize the above configuration to creat } $sessionManager = new SessionManager($sessionConfig, $sessionStorage, $sessionSaveHandler); - - if (isset($session['validators'])) { - $chain = $sessionManager->getValidatorChain(); - foreach ($session['validators'] as $validator) { - $validator = new $validator(); - $chain->attach('session.validate', array($validator, 'isValid')); - - } - } } else { $sessionManager = new SessionManager(); } diff --git a/docs/languages/en/modules/zend.validator.set.rst b/docs/languages/en/modules/zend.validator.set.rst index aa01fbddf..e7f400719 100644 --- a/docs/languages/en/modules/zend.validator.set.rst +++ b/docs/languages/en/modules/zend.validator.set.rst @@ -11,7 +11,6 @@ Zend Framework comes with a standard set of validation classes, which are ready .. include:: zend.validator.between.rst .. include:: zend.validator.callback.rst .. include:: zend.validator.credit-card.rst -.. _zend.validator.set.ccnum: Ccnum ----- diff --git a/docs/languages/en/snippets.rst b/docs/languages/en/snippets.rst index bc0e1ccdb..1ed59706d 100644 --- a/docs/languages/en/snippets.rst +++ b/docs/languages/en/snippets.rst @@ -11,6 +11,11 @@ you various techniques and features of the framework in order to build an application. +.. |InDepthTutorial| replace:: In-depth tutorial for beginners + +.. |InDepthTutorialIntroduction| replace:: In this tutorial we will create a Blog-Application from scratch. We will go + through all the details you need to learn to create your own ZF2 Application. + .. |GettingStartedWithZendStudio| replace:: Getting Started With Zend Studio 10 & Zend Server 6 .. |GettingStartedWithZendStudioIntroduction| replace:: The user guide is provided to take you through