Showing posts with label PHP. Show all posts
Showing posts with label PHP. Show all posts

Sunday, October 18, 2020

Sleeping is not the best option

Introduction.

Some time ago we were developing a code that stored some data with a given TTL. We wanted to check not only that the data was stored correctly but also that it expired after the given TTL. This is an example of testing asynchronous code.

When testing asynchronous code we need to carefully coordinate the test with the system it is testing to avoid running the assertion before the tested action has completed[1]. For example, the following test will always fail because the assertion in line 30 is checked before the data has expired:

In this case the test always fails but in other cases it might be worse, failing intermittently when the system is working, or passing when the system is broken. We need to make the test wait to give the action we are testing time to complete successfully and fail if this doesn’t happen within a given timeout period.

Sleeping is not the best option.

This is an improved version of the previous test in which we are making the test code wait before the checking that the data has expired to give the code under test time to run:

The problem with the simple sleeping approach is that in some runs the timeout might be enough for the data to expire but in other runs it might not, so the test will fail intermittently; it becomes a flickering test. Flickering tests are confusing because when they fail, we don’t know whether it’s due to a real bug, or it is just a false positive. If the failure is relatively common, the team might start ignoring those tests which can mask real defects and completely destroy the value of having automated tests.

Since the intermittent failures happen because the timeout is too close to the time the behavior we are testing takes to run, many teams decide to reduce the frequency of those failures by increasing the time each test sleeps before checking that the action under test was successful. This is not practical because it soon leads to test suites that take too long to run.

Alternative approaches.

If we are able to detect success sooner, succeeding tests will provide rapid feedback, and we only have to wait for failing tests to timeout. This is a much better approach than waiting the same amount of time for each test regardless it fails or succeeds.

There are two main strategies to detect success sooner: capturing notifications[2] and polling for changes.

In the case we are using as an example, polling was the only option because redis didn’t send any monitoring events we could listen to.

Polling for changes.

To detect success as soon as possible, we’re going to probe several times separated by a time interval which will be shorter than the previous timeout. If the result of a probe is what we expect the test pass, if the result we expect is not there yet, we sleep a bit and retry. If after several retries, the expected value is not there, the test will fail.

Have a look at the checkThatDataHasExpired method in the following code:

By polling for changes we avoid always waiting the maximum amount of time. Only in the worst case scenario, when consuming all the retries without detecting success, we’ll wait as much as in the just sleeping approach that used a fixed timeout.

Extracting a helper.

Scattering ad hoc low level code that polls and probes like the one in checkThatDataHasExpired throughout your tests not only make them difficult to understand, but also is a very bad case of duplication. So we extracted it to a helper so we could reuse it in different situations.

What varies from one application of this approach to another are the probe, the check, the number of probes before failing and the time between probes, everything else we extracted to the following helper[3]:

This is how the previous tests would look after using the helper:

Notice that we’re passing the probe, the check, the number of probes and the sleep time between probes to the AsyncTestHelpers::assertWithPolling function.

Conclusions.

We showed an example in Php of an approach to test asynchronous code described by Steve Freeman and Nat Pryce in their Growing Object-Oriented Software, Guided by Tests book. This approach avoids flickering test and produces much faster test suites than using a fixed timeout. We also showed how we abstracted this approach by extracting a helper function that we are reusing in our code.

We hope you’ve found this approach interesting. If you want to learn more about this and several other techniques to effectively test asynchronous code, have a look at the wonderful Growing Object-Oriented Software, Guided by Tests book[4].

Acknowledgements.

Thanks to my Codesai colleagues for reading the initial drafts and giving me feedback and to Chrisy Totty for the lovely cat picture.

Notes.

[1] This is a nice example of Connascence of Timing (CoT). CoT happens when when the timing of the execution of multiple components is important. In this case the action being tested must run before the assertion that checks its observable effects. That's the coordination we talk about. Check our post about Connascence to learn more about this interesting topic.
[2] In the capturing notifications strategy the test code "observes the system by listening for events that the system sends out. An event-based assertion waits for an event by blocking on a monitor until gets notified or times out", (from Growing Object-Oriented Software, Guided by Tests book).
Some time ago we developed some helpers using the capturing notifications strategy to test asynchronous ClojureScript code that was using core.async channels. Have a look at, for instance, the expect-async-message assertion helper in which we use core.async/alts! and core.async/timeout to implement this behaviour. The core.async/alts! function selects the first channel that responds. If that channel is the one the test code was observing we assert that the received message is what we expected. If the channel that responds first is the one generated by core.async/timeout we fail the test. We mentioned these async-test-tools in previous post: Testing Om components with cljs-react-test.
[3] Have a look at the testing asynchronous systems examples in the GOOS Code examples repository for a more object-oriented implementation of helper for the polling for changes strategy, and also examples of the capturing notifications strategy.
[4] Chapter 27, Testing Asynchronous code, contains a great explanation of the two main strategies to test asynchronous code effectively: capturing notifications and polling for changes.

References.

Thursday, June 27, 2019

An example of listening to the tests to improve a design

Introduction.

Recently in the B2B team at LIFULL Connect, we improved the validation of the clicks our API receive using a service that detects whether the clicks were made by a bot or a human being.

So we used TDD to add this new validation to the previously existing validation that checked if the click contained all mandatory information. This was the resulting code:

and these were its tests:

The problem with these tests is that they know too much. They are coupled to many implementation details. They not only know the concrete validations we apply to a click and the order in which they are applied, but also details about what gets logged when a concrete validations fails. There are multiple axes of change that will make these tests break. The tests are fragile against those axes of changes and, as such, they might become a future maintenance burden, in case changes along those axes are required.

So what might we do about that fragility when any of those changes come?

Improving the design to have less fragile tests.

As we said before the test fragility was hinting to a design problem in the ClickValidation code. The problem is that it’s concentrating too much knowledge because it’s written in a procedural style in which it is querying every concrete validation to know if the click is ok, combining the result of all those validations and knowing when to log validation failures. Those are too many responsibilities for ClickValidation and is the cause of the fragility in the tests.

We can revert this situation by changing to a more object-oriented implementation in which responsibilities are better distributed. Let’s see how that design might look:

1. Removing knowledge about logging.

After this change, ClickValidation will know nothing about looging. We can use the same technique to avoid knowing about any similar side-effects which concrete validations might produce.

First we create an interface, ClickValidator, that any object that validates clicks should implement:

Next we create a new class NoBotClickValidator that wraps the BotClickDetector and adapts[1] it to implement the ClickValidator interface. This wrapper also enrichs BotClickDetector’s’ behavior by taking charge of logging in case the click is not valid.

These are the tests of NoBotClickValidator that takes care of the delegation to BotClickDetector and the logging:

If we used NoBotClickValidator in ClickValidation, we’d remove all knowledge about logging from ClickValidation.

Of course, that knowledge would also disappear from its tests. By using the ClickValidator interface for all concrete validations and wrapping validations with side-effects like logging, we’d make ClickValidation tests robust to changes involving some of the possible axis of change that were making them fragile:

  1. Changing the interface of any of the individual validations.
  2. Adding side-effects to any of the validations.

2. Another improvement: don't use test doubles when it's not worth it[2].

There’s another way to make ClickValidation tests less fragile.

If we have a look at ClickParamsValidator and BotClickDetector (I can’t show their code here for security reasons), they have very different natures. ClickParamsValidator has no collaborators, no state and a very simple logic, whereas BotClickDetector has several collaborators, state and a complicated validation logic.

Stubbing ClickParamsValidator in ClickValidation tests is not giving us any benefit over directly using it, and it’s producing coupling between the tests and the code.

On the contrary, stubbing NoBotClickValidator (which wraps BotClickDetector) is really worth it, because, even though it also produces coupling, it makes ClickValidation tests much simpler.

Using a test double when you’d be better of using the real collaborator is a weakness in the design of the test, rather than in the code to be tested.

These would be the tests for the ClickValidation code with no logging knowledge, after applying this idea of not using test doubles for everything:

Notice how the tests now use the real ClickParamsValidator and how that reduces the coupling with the production code and makes the set up simpler.

3. Removing knowledge about the concrete sequence of validations.

After this change, the new design will compose validations in a way that will result in ClickValidation being only in charge of combining the result of a given sequence of validations.

First we refactor the click validation so that the validation is now done by composing several validations:

The new validation code has several advantages over the previous one:

  • It does not depend on concrete validations any more
  • It does not depend on the order in which the validations are made.

It has only one responsibility: it applies several validations in sequence, if all of them are valid, it will accept the click, but if any given validation fails, it will reject the click and stop applying the rest of the validations. If you think about it, it’s behaving like an and operator.

We may write these tests for this new version of the click validation:

These tests are robust to the changes making the initial version of the tests fragile that we described in the introduction:

  1. Changing the interface of any of the individual validations.
  2. Adding side-effects to any of the validations.
  3. Adding more validations.
  4. Changing the order of the validation.

However, this version of ClickValidationTest is so general and flexible, that using it, our tests would stop knowing which validations, and in which order, are applied to the clicks[3]. That sequence of validations is a business rule and, as such, we should protect it. We might keep this version of ClickValidationTest only if we had some outer test protecting the desired sequence of validations.

This other version of the tests, on the other hand, keeps protecting the business rule:

Notice how this version of the tests keeps in its setup the knowledge of which sequence of validations should be used, and how it only uses test doubles for NoBotClickValidator.

4. Avoid exposing internals.

The fact that we’re injecting into ClickValidation an object, ClickParamsValidator, that we realized we didn’t need to double, it’s a smell which points to the possibility that ClickParamsValidator is an internal detail of ClickValidation instead of its peer. So by injecting it, we’re coupling ClickValidation users, or at least the code that creates it, to an internal detail of ClickValidation: ClickParamsValidator.

A better version of this code would hide ClickParamsValidator by instantiating it inside ClickValidation’s constructor:

With this change ClickValidation recovers the knowledge of the sequence of validations which in the previous section was located in the code that created ClickValidation.

There are some stereotypes that can help us identify real collaborators (peers)[4]:

  1. Dependencies: services that the object needs from its environment so that it can fulfill its responsibilities.
  2. Notifications: other parts of the system that need to know when the object changes state or performs an action.
  3. Adjustments or Policies: objects that tweak or adapt the object’s behaviour to the needs of the system.

Following these stereotypes, we could argue that NoBotClickValidator is also an internal detail of ClickValidation and shouldn’t be exposed to the tests by injecting it. Hiding it we’d arrive to this other version of ClickValidation:

in which we have to inject the real dependencies of the validation, and no internal details are exposed to the client code. This version is very similar to the one we’d have got using tests doubles only for infrastructure.

The advantage of this version would be that its tests would know the least possible about ClickValidation. They’d know only ClickValidation’s boundaries marked by the ports injected through its constructor, and ClickValidation`’s public API. That will reduce the coupling between tests and production code, and facilitate refactorings of the validation logic.

The drawback is that the combinations of test cases in ClickValidationTest would grow, and may of those test cases would talk about situations happening in the validation boundaries that might be far apart from ClickValidation’s callers. This might make the tests hard to understand, specially if some of the validations have a complex logic. When this problem gets severe, we may reduce it by injecting and use test doubles for very complex validators, this is a trade-off in which we decide to accept some coupling with the internal of ClickValidation in order to improve the understandability of its tests. In our case, the bot detection was one of those complex components, so we decided to test it separately, and inject it in ClickValidation so we could double it in ClickValidation’s tests, which is why we kept the penultimate version of ClickValidation in which we were injecting the click-not-made-by-a-bot validation.

Conclusion.

In this post, we tried to play with an example to show how listening to the tests[5] we can detect possible design problems, and how we can use that feedback to improve both the design of our code and its tests, when changes that expose those design problems are required.

In this case, the initial tests were fragile because the production code was procedural and had too many responsibilities. The tests were fragile also because they were using test doubles for some collaborators when it wasn’t worth to do it.

Then we showed how refactoring the original code to be more object-oriented and separating better its responsibilities, could remove some of the fragility of the tests. We also showed how reducing the use of test doubles only to those collaborators that really needs to be substituted can improve the tests and reduce their fragility. Finally, we showed how we can go too far in trying to make the tests flexible and robust, and accidentally stop protecting a business rule, and how a less flexible version of the tests can fix that.

When faced with fragility due to coupling between tests and the code being tested caused by using test doubles, it’s easy and very usual to “blame the mocks”, but, we believe, it would be more productive to listen to the tests to notice which improvements in our design they are suggesting. If we act on this feedback the tests doubles give us about our design, we can use tests doubles in our advantage, as powerful feedback tools[6], that help us improve our designs, instead of just suffering and blaming them.

Acknowledgements.

Many thanks to my Codesai colleagues Alfredo Casado, Fran Reyes, Antonio de la Torre and Manuel Tordesillas, and to my Aprendices colleagues Paulo Clavijo, Álvaro García and Fermin Saez for their feedback on the post, and to my colleagues at LIFULL Connect for all the mobs we enjoy together.

Footnotes:

[2] See Test Smell: Everything is mocked by Steve Freeman where he talks about things you shouldn't be substituting with tests doubles.
[3] Thanks Alfredo Casado for detecting that problem in the first version of the post.
[4] From Growing Object-Oriented Software, Guided by Tests > Chapter 6, Object-Oriented Style > Object Peer Stereotypes, page 52. You can also read about these stereotypes in a post by Steve Freeman: Object Collaboration Stereotypes.
[5] Difficulties in testing might be a hint of design problems. Have a look at this interesting series of posts about listening to the tests by Steve Freeman.
[6] According to Nat Pryce mocks were designed as a feedback tool for designing OO code following the 'Tell, Don't Ask' principle: "In my opinion it's better to focus on the benefits of different design styles in different contexts (there are usually many in the same system) and what that implies for modularisation and inter-module interfaces. Different design styles have different techniques that are most applicable for test-driving code written in those styles, and there are different tools that help you with those techniques. Those tools should give useful feedback about the external and *internal* quality of the system so that programmers can 'listen to the tests'. That's what we -- with the help of many vocal users over many years -- designed jMock to do for 'Tell, Don't Ask' object-oriented design." (from a conversation in Growing Object-Oriented Software Google Group).

I think that if your design follows a different OO style, it might be preferable to stick to a classical TDD style which nearly limits the use of test doubles only to infrastructure and undesirable side-effects.

Saturday, May 25, 2019

The curious case of the negative builder

Recently, one of the teams I’m coaching at my current client, asked me to help them with a problem, they were experiencing while using TDD to add and validate new mandatory query string parameters[1]. This is a shortened version (validating fewer parameters than the original code) of the tests they were having problems with:

and this is the implementation of the QueryStringBuilder used in this test:

which is a builder with a fluid interface that follows to the letter a typical implementation of the pattern. There are even libraries that help you to automatically create this kind of builders[2].

However, in this particular case, implementing the QueryStringBuilder following this typical recipe causes a lot of problems. Looking at the test code, you can see why.

To add a new mandatory parameter, for example sourceId, following the TDD cycle, you would first write a new test asserting that a query string lacking the parameter should not be valid.

So far so good, the problem comes when you change the production code to make this test pass, in that momento you’ll see how the first test that was asserting that a query string with all the parameters was valid starts to fail (if you check the query string of that tests and the one in the new test, you’ll see how they are the same). Not only that, all the previous tests that were asserting that a query string was invalid because a given parameter was lacking won’t be “true” anymore because after this change they could fail for more than one reason.

So to carry on, you’d need to fix the first test and also change all the previous ones so that they fail again only for the reason described in the test name:

That’s a lot of rework on the tests only for adding a new parameter, and the team had to add many more. The typical implementation of a builder was not helping them.

The problem we’ve just explained can be avoided by chosing a default value that creates a valid query string and what I call “a negative builder”, a builder with methods that remove parts instead of adding them. So we refactored together the initial version of the tests and the builder, until we got to this new version of the tests:

which used a “negative” QueryStringBuilder:

After this refactoring, to add the sourceId we wrote this test instead:

which only carries with it updating the valid method in QueryStringBuilder and adding a method that removes the sourceId parameter from a valid query string.

Now when we changed the code to make this last test pass, no other test failed or started to have descriptions that were not true anymore.

Leaving behind the typical recipe and adapting the idea of the builder pattern to the context of the problem at hand, led us to a curious implementation, a “negative builder”, that made the tests easier to maintain and improved our TDD flow.

Acknowledgements.

Many thanks to my Codesai colleagues Antonio de la Torre and Fran Reyes, and to all the colleagues of the Prime Services Team at LIFULL Connect for all the mobs we enjoy together.

Footnotes:

[1] Currently, this validation is not done in the controller anymore. The code showed above belongs to a very early stage of an API we're developing.
[2] Have a look, for instance, at lombok's' @Builder annotation for Java.

Tuesday, April 1, 2014

Interesting Talk: "Testing Online Crazy Glue: Strategies for building testable PHP applications"

I've just watched this interesting talk by Chris Hartjes:

Introduced FormEmailService façade and struggling with names again

Following the advice given by One on a comment, I introduced a façade service that encapsulate everything about emails, FormEmailService with FormEmailComposer and EmailSender as collaborators. This simplified the creation of the IntroducedInformation class which was great.

I also followed his suggestion of passing the form object as a parameter to the sendByEmailAndRedirect method.

This was how it was used after that change:

Although the creation of the IntroducedInformation got simpler, I didn't like how it read anymore because an "information thing" was now sending a form.

In the previous version the "information thing" was sending itself using a "Yoda-like" language:

To keep the form as a parameter I decided to rename the class to so it was "something that does something on something" and not finding any better name I went down the road of "SomethingService" names:

Well, it's not perfect but at least I think it reads better than the version where the "information thing" was sending a form.

This is how the class looks now:

and these are its tests:
I keep thinking in a name for this that doesn't follow the "SomethingService" pattern, but until inspiration comes I'll settle on this one.

Thank you very much Carlos, Marcin and José for your feedback. I really appreciate it.

Monday, March 31, 2014

Finally some better naming but still with fragile tests

After sleeping on it I've found better names for the examples I've recently posted about (see preious post).

The obstacle was seeing it as an action on a form. Thinking instead on information that needs to be introduced and sent made the names flow much better.

This is the resulting class:
and these are its tests:


Finally, this is how it's used now:



I'm much happier with the naming now.

The tests are still fragile, but by using helpers in the tests the amount of places where the mocks are used gets reduced.

Refactored my clumsy tests

I've reworked the class and the tests I showed in my previous post.

This is the refactored class:

The responsibility to compose an email from the form fields was delegated to the FormEmailComposer collaborator whereas the responsibility of sending the email was delegated to the EmailSender collaborator.

The Redirection collaborator is still in charge of the redirecting responsibility.

Now the Form and the Email class are just value objects.

These are the new tests:

And this is how I'm using the SendingAndRedirectionAction class:

I'm a bit happier with the naming and I also reduced the number of classes.
However I'm still concerned about the tests fragility.

Saturday, March 29, 2014

First clumsy tests using Phake test doubles framework

Two weeks ago I started learning PHP for my new job at Runroom.

It's my first job as a web developer so I'm still a bit "lost".

Today I tried to write unit test for this class in order to learn how to use Phake test doubles framework:
These were the resulting tests:
Although I like the readability of the tests, I'm not very happy with them because I think they will be too fragile. At the same time I'm not sure how else I could test the behavior of SendingFormByEmailAndRedirecting class...

To give you a bit more of context this is how I'm using it from my code:

Thursday, February 20, 2014

Interesting Talk: "The Framework as an implementation detail"

I've just watched this interesting talk by Konstantin Kudryashov and Marcello Duarte:
They show how the framework (in this case Symfony) can become an implementation detail by using the hexagonal architecture.

Check also the example code they show at the end of the talk.

Saturday, March 27, 2010

Agenda mensual con calendario en PHP (y V)

Por último la clase Mes.
Las clases van contenidas unas dentro de otras a modo de "matrioska".
La clase Mes es la que conecta con la BD.
/* Clase Mes */
class Mes
{
    private $anyo;    
    private $mes;
    private $numDiasMes;
    private $numSemanasMes;
    private $semanas;

    
    /* Constructor */
    public function __construct($anyo, $mes)  
    {
        //Inicializo los miembros
        $this->setAnyo($anyo);
        $this->setMes($mes);
        $this->numDiasMes = Fechas::getFinMes($anyo,$mes);
        $this->crearSemanasMes();  
        $this->addEventos();
    }       
    
    /* Métodos de acceso */
    public function setAnyo($anyo) 
    {
        $this->anyo = $anyo;
    }

    public function getAnyo() 
    {
        return $this->anyo;
    }
    
    public function setMes($mes) 
    {
        $this->mes = $mes;
    }

    public function getMes() 
    {
        return $this->mes;
    }

    public function getNumDiasMes() 
    {
        return $this->numDiasMes;
    }

    public function getNumSemanasMes() 
    {
        return $this->numSemanasMes;
    }
   
    public function getSemanas() 
    {
        return $this->semanas;
    }
    
    /* Métodos */
    
    /* Genera las semanas de un mes */
    private function crearSemanasMes()
    {   
        /*Se inicializan las variables antes del bucle*/
        
        $i = 0;
        
        /*Dia en que nos encontramos al recorrer la agenda*/
        $diaActual = 1; //Se inicializa a 1
        
        /*Mientras no hayamos entrado en otro mes*/
        while( $diaActual <= $this->numDiasMes )
        {
            /*Se crean las semanas*/  
            $this->semanas[$i] = new Semana();
            
            $this->semanas[$i]->creaDiasSemana($diaActual, $this->anyo,
                                               $this->mes,$this->numDiasMes);
            
            $i = $i + 1;
        }  
        
        /*Se guarda el número de semanas del mes*/
        $this->numSemanasMes = $i;
       
    }
    
    /* Añade un evento a la agenda */
    public function addEvento($dia, $evento) 
    {
        $numSemanaIni = Fechas::getSemanaAnyo($this->anyo, $this->mes, 1);
        
        $numSemana = Fechas::getSemanaAnyo($this->anyo, $this->mes, $dia);
        
        //Si la primera semana del año es la 59
        if(53==$numSemanaIni)
        {
            $numSemanaIni = 0;
            
            //Si además el evento es esa semana
            if(53==$numSemana)
            {
                $numSemana = 0;    
            }
        }
        
        $indiceSemana = $numSemana - $numSemanaIni;
        
        $diaSemana = Fechas::getDiaSemana($this->anyo, $this->mes, $dia);
        
        $this->semanas[$indiceSemana]->setEvento($evento, $diaSemana);
    }
    
    /* Obtiene un evento de la agenda */
    public function getEvento($dia) 
    {
        $numSemanaIni = Fechas::getSemanaAnyo($this->anyo, $this->mes, 1);
    
        $numSemana = Fechas::getSemanaAnyo($this->anyo, $this->mes, $dia);
        
        //Si la primera semana del año es la 59
        if(53==$numSemanaIni)
        {
            $numSemanaIni = 0;
            
            //Si además el evento es esa semana
            if(53==$numSemana)
            {
                $numSemana = 0;    
            }
        }
        
        $indiceSemana = $numSemana - $numSemanaIni;
        
        $diaSemana = Fechas::getDiaSemana($this->anyo, $this->mes, $dia);           
        
        return $this->semanas[$indiceSemana]->getEvento($diaSemana);
    }
    
    /* Muestra un evento de la agenda */
    public function muestraEvento($dia) 
    {
        $numSemanaIni = Fechas::getSemanaAnyo($this->anyo, $this->mes, 1);
    
        $numSemana = Fechas::getSemanaAnyo($this->anyo, $this->mes, $dia);
        
        //Si la primera semana del año es la 59
        if(53==$numSemanaIni)
        {
            $numSemanaIni = 0;
            
            //Si además el evento es esa semana
            if(53==$numSemana)
            {
                $numSemana = 0;    
            }
        }
        
        $indiceSemana = $numSemana - $numSemanaIni;
        
        $diaSemana = Fechas::getDiaSemana($this->anyo, $this->mes, $dia);           
        
        $this->semanas[$indiceSemana]->muestraEvento($diaSemana);
    }
    
    /* Muestra el HTML de un mes */
    public function muestraCalendario()
    {   
        $nombreMeses = array('Enero','Febrero','Marzo','Abril','Mayo','Junio','Julio',
                             'Agosto','Septiembre','Octubre','Noviembre','Diciembre');
        $nombreDias = array('Lun','Mar','Mie','Jue','Vie','Sab','Dom');                                  
        
        
        /*Se obtiene el nombre del mes*/
        $nombreMes = $nombreMeses[$this->mes - 1];
        
        
        /*Se colocan los botones*/
        printf("<table class=\"tablaBotones\"><tr><td>
                <input type=\"submit\" name=\"restaMes\" value=\"<<\" onClick=\"verMesAnterior()\">
                </td><td class=\"caption_text\">%s %d</td><td>
                <input type=\"submit\" name=\"sumaMes\" value=\">>\" onClick=\"verMesSiguiente()\">
                </td></tr><table>\n", $nombreMes, $this->anyo);
                
        /*Se abre la tabla*/
        printf("<table>\n");
        
        /*Se abre el cuerpo de la tabla*/ /*Se abre la tabla*/
        printf("<tbody>\n");
        /*Se crea la primera fila de la tabla*/
        printf("<tr>\n");
        
        /*La primera casilla va vacía*/
        printf("<th class=\"dia\"></th>\n");
        
        /*Se generan las casilla de los días de la semana*/
        for($i=0;$i<7;$i++)
        {   
           /*Se escribe en el HTML el texto del día de la semana */
           printf("<th class=\"dia\"> %s </th>\n", $nombreDias[$i]);     
        }
     
        printf("</tr>\n");

        /* Semanas */
        $this->muestraSemanas();
        
        /*Se cierra el cuerpo de la tabla*/
        printf("</tbody>\n");
        
        /*Se cierra la tabla*/
        printf("</table>\n");

    }
    
    /* Genera el HTML de todas las semanas del mes */
    public function muestraSemanas()
    {
        foreach($this->semanas as $semana)
        {
            $semana->muestraSemana();
        }
    }
    
    /* Añade al calendario del mes los eventos que están en la BD */
    public function addEventos()
    {
        require_once 'login.php';
        
        //Se intenta conectar con la BD
        $db_server = mysql_connect($db_hostname,$db_username,$db_password);
                
        //Si ha habido errores se aborta y se informa al usuario
        if(!$db_server)
        {
            die( "No se pudo conectar con MySQL: " . mysql_error() );
        }
        
        mysql_select_db($db_database)
            or die( "No se pudo selecionar la BD: " . mysql_error() );
        
        //Consulta SQL
        $query = "select id_evento, nombre, DATE_FORMAT(fecha_hora,'%d-%m-%y'),
                  TIME_FORMAT(fecha_hora,'%H:%k'), descripcion, lugar, DAY(fecha_hora)
                  from calendariov where MONTH(fecha_hora) = '$this->mes'
                  and YEAR(fecha_hora) = '$this->anyo'";
        
        $result = mysql_query($query);

        //Si hubo fallos se informa al usuario
        if(!$result)
        {
            die("Fallo en el acceso a la BD: " . mysql_error());
        }

        //Si todo fue bien
        $numEventos = mysql_num_rows($result);

        for($j = 0;$j < $numEventos; ++$j)
        {
            $row = mysql_fetch_row($result);
                      
            //Se inserta el evento en el calendario
            $this->addEvento( (int) $row[6], new Evento($row[0], $row[1], $row[2], 
                                                        $row[3], $row[4], $row[5]));
        }

        mysql_close($db_server);

    }
            
}  

?>     

Agenda mensual con calendario en PHP (IV)

Esta es la clase Semana.
/* Clase Semana */
class Semana
{
    /* Miembros */
    private $dias;
    private $numSemanaAnyo;
    
    /* Métodos de acceso */
    public function getNumSemanaAnyo() 
    {
        return $this->numSemanaAnyo;
    }
    
    public function getDias() 
    {
        return $this->dias;
    }
    
    /* Metodos */
    
    /* Genera los días de una semana */
    public function creaDiasSemana(&$diaActual, $anyo, $mes, $finMes)
    {          
        /*Se crea la semana*/
        $this->numSemanaAnyo = Fechas::getSemanaAnyo($anyo, $mes, $diaActual);
         
        $diaSemana = Fechas::getDiaSemana($anyo, $mes, $diaActual);
        
        /*Se crea el dia actual*/
        $diaActAux = $diaActual; 
        
        /*Se calcula lo que hay que retroceder*/
        $atras = $diaSemana - 1;
        
        /*Retrocedemos*/
        for($i = 0; $i < $atras; $i++)
        {
            /*Se crea un nuevo día*/
            $this->dias[$diaSemana - 2 - $i] = new Dia($diaSemana - 1 - $i,0,false);
        }
             
        /*Se calcula lo que hay que avanzar*/
        $adelante= 7 - $diaSemana + 1;
        
        /*Avanzamos*/
        for($i = 0; $i < $adelante; $i++)
        {   
            /*Se comprueba si el día está dentro del mes*/
            $perteneceAlMes = ( $diaActAux <= $finMes );

            /*Se crea un nuevo día*/
            $this->dias[$diaSemana - 1 + $i] = new Dia($diaSemana + $i, $diaActAux, $perteneceAlMes);
            
            /*Se incrementa el valor del día actual si se está dentro del mes*/    
            $diaActAux = $diaActAux + 1;
        }
         
        /*Se actualiza el día actual de forma que la próxima semana empiece en Lunes*/
        $diaActual = $diaActual + $adelante;
        return;
    }
    
    public function setEvento($evento, $diaSemana)
    {
        $this->dias[$diaSemana - 1]->setEvento($evento);
    }
    
    public function getEvento($diaSemana)
    {
        return $this->dias[$diaSemana - 1]->getEvento();
    }
    
    public function muestraEvento($diaSemana)
    {
        $this->dias[$diaSemana - 1]->muestraEvento();
    }
    
    /* Muestra el HTML correspondiente a una semana */
    public function muestraSemana()
    {
        /*Se abre la fila*/
        printf("<tr>\n");
        
        /*La primera columna lleva el número de semana*/
        printf("<td class=\"semana\"> %d </td>\n", $this->numSemanaAnyo);
        
        /*Se genera el HTML de los días*/
        foreach($this->dias as $dia)
        {
            $dia->muestraDia();
        }
        
        /*Se cierra la fila*/
        printf("</tr>\n"); 
    }
}

Thursday, March 25, 2010

Agenda mensual con calendario en PHP (III)

Esta es la clase para los días.

En este caso lo he limitado a un evento por día pero usando un array se puede modificar para recoger varios eventos por día.

/* Clase Dia */
class Dia
{   
    /* Miembros */
    private $perteneceAlMes; //Indica si pertenece al mes que muestra en el calendario
    private $numDia;         //Número del día en el mes
    private $diaSemana;      //Número del día de la semana
    private $evento;         //Objeto de la clase evento que contiene los datos asociados a un evento
    private $hayEvento;
    
    /* Constructor */
    public function __construct($diaSemana, $diaActual, $perteneceAlMes)  
    {

        //Pertenece al mes
        $this->setPerteneceAlMes($perteneceAlMes);
        
        //Día del mes
        if(!$perteneceAlMes)
        {
           $this->setNumDia(-1);
        }
        else
        {
           $this->setNumDia($diaActual);
        }
        
        //Día de la semana
        $this->setDiaSemana($diaSemana);
        
        //En principio no hay ningún evento
        $this->setHayEvento(false);
        
    }

    /* Métodos de acceso */
    public function setHayEvento($hayEvento) 
    {
        $this->hayEvento = $hayEvento;
    }
    
    public function getHayEvento() 
    {
        return $this->hayEvento;
    }
    
    public function setPerteneceAlMes($perteneceAlMes) 
    {
        $this->perteneceAlMes = $perteneceAlMes;
    }
    
    public function getPerteneceAlMes() 
    {
        return $this->perteneceAlMes;
    }

    public function setNumDia($numDia) 
    {
        $this->numDia = $numDia;
    }

    public function getNumDia() 
    {
        return $this->numDia;
    }
    
    public function setDiaSemana($diaSemana) 
    {
        $this->diaSemana = $diaSemana;
    }
    
    public function setEvento($evento) 
    {
        $this->evento = $evento;
        $this->setHayEvento(true); 
    }

    public function getEvento() 
    {
        return $this->evento;
    }
    
    public function muestraEvento()
    {
        $this->evento->muestraEvento();
    }
    
    /* Métodos */
    
    /* Muestra el HTML correspondiente a un día */
    public function muestraDia()
    {
        //Si el día pertenece al mes    
        if($this->getPerteneceAlMes())
        {
            //Si hay un evento en el día
            if($this->getHayEvento())
            {
                $numDia = $this->getNumDia();
                printf( "<td class =\"hayEvento\"<a href=\"#\" onClick=\"verEvento(%d)\">
                         %d</a></td>\n", 
                         $numDia, $numDia );                           
            }
            //Si no lo hay
            else
            {
                printf( "<td class=\"noHayEvento\">%d</td>\n", $this->getNumDia() );
            }
        }
        //Si el día no pertenece al mes
        else
        {
            printf("<td></td>\n");
        }
    }
}

Tuesday, March 23, 2010

Agenda mensual con calendario en PHP (II)

Esta clase la uso para los eventos de la agenda:
    
/* Clase Evento */
class Evento
{
    /* Miembros */
    private $referencia;
    private $nombre;
    private $fecha;
    private $hora;
    private $lugar;
    private $descripcion;
    
    /* Constructor */
    public function __construct($referencia, $nombre, $fecha, $hora, $descripcion, $lugar)  
    {
        $this->setReferencia($referencia);
        $this->setNombre($nombre);
        $this->setFecha($fecha);
        $this->setHora($hora);
        $this->setLugar($lugar);
        $this->setDescripcion($descripcion);
    }
    
    /* Métodos de acceso */
    public function getReferencia()
    {
        return $this->referencia;
    }
    
    public function setReferencia($referencia)
    {
        $this->referencia = $referencia;
    }
    
    public function getNombre()
    {
        return $this->nombre;
    }
    
    public function setNombre($nombre)
    {
        $this->nombre = $nombre;
    }
    
    public function getFecha()
    {
        return $this->fecha;
    }
    
    public function setFecha($fecha)
    {
        $this->fecha = $fecha;
    }
    
    public function getHora()
    {
        return $this->hora;
    }
    
    public function setHora($hora)
    {
        $this->hora = $hora;
    }
    
    public function getLugar()
    {
        return $this->lugar;
    }
    
    public function setLugar($lugar)
    {
        $this->lugar = $lugar;
    }
    
    public function getDescripcion()
    {
        return $this->descripcion;
    }
    
    public function setDescripcion($descripcion)
    {
        $this->descripcion = $descripcion;
    }
    
    /* Métodos */
    public function muestraEvento()
    {
       printf("%s \n", $this->getNombre());
       printf("Fecha: %s \n", $this->getFecha());
       printf("Hora: %s \n", $this->getHora());
       printf("Lugar: %s \n", $this->getLugar());
       printf("Descripción: %s \n",
                                              $this->getDescripcion());   
    }
}

Agenda mensual con calendario en PHP (I)

Hace poco hice un calendario en PHP para prácticar.

Estas son algunas de las clases que creé:

Esta es una clase con métodos estáticos que sirven para hacer diferentes cálculos con fechas.
/* Clase Fechas */
class Fechas
{
    /* Obtiene el día de la semana a partir del año, del mes y del día */
    public static function getDiaSemana($anyo, $mes, $dia)
    {               
        /*Módulos acumulados del número de días antes del inicio de mes para años normales y bisisestos*/
        $modMeses = array(0,3,3,6,1,4,6,2,5,0,3,5);
        $modMesesBis = array(0,3,4,0,2,5,0,3,6,1,4,6); 
        
        /*Se comprueba si el año es bisiesto para saber cual de los dos vectores anteriores usar*/
        if( 1 == Fechas::esBisiesto($anyo))
        {
            $dMes = $modMesesBis[$mes-1];
        }
        else
        {
            $dMes = $modMeses[$mes-1];
        }
        
        /*Se calcula el día de la semana correspondiente a la fecha*/
        $divEntera1 = (int) ( ($anyo - 1.0) / 4.0 ); 
        $divEntera2 = (int) ( ($anyo - 1.0) / 100.0 );
        $divEntera3 = (int) ( ($divEntera2 + 1.0) / 4.0 );
        $divEntera4 =(int) ( $divEntera1 - 3 * $divEntera3 );
        
        /*Fórmula del día de la semana*/
        $diaSemana = ( (($anyo - 1) % 7) + ($divEntera4 % 7) + $dMes + ($dia % 7)  ) % 7;
        
        /*El 0 corresponde al Domingo*/
        if( 0 == $diaSemana) 
        {
            $diaSemana = 7;
        }
        return $diaSemana; 
    }
    
    /* Calcula la semana del año a partir del día, el mes y el año */
    public static function getSemanaAnyo($anyo, $mes, $dia)
    {   
        //Valores de los días acumulados antes del principio de cada mes
        $ordinalesNormal=array(0, 31, 59,  90, 120, 151, 181, 212, 243, 273, 304, 334);
        $ordinalesBisiesto=array(0,  31,  60,  91, 121, 152, 182, 213, 244, 274, 305, 335);
        
        //Según el año sea bisiesto o no se usan unos valores u otros
        if( 1 == Fechas::esBisiesto($anyo))
        {
           $ordinal= $ordinalesBisiesto[$mes - 1];
        }
        {
           $ordinal= $ordinalesNormal[$mes - 1];
        }
        
        //Se calcula el día ordinal
        $ordinal = $ordinal + $dia;
        
        //Se obtiene el número del día de la semana correspondiente a la fecha (L=1,...,D=7)
        $diaSemana = Fechas::getDiaSemana($anyo,$mes,$dia);
        
        //Se calcula la semana del año
        $semanaAnyo=(int) (( $ordinal - $diaSemana + 10)/7);
        
        //Si es cero quiere decir que es la última semana del año anterior
        if($semanaAnyo == 0)
        {
            $semanaAnyo = 53;
        }
        //Si es 53 es la última semana del año anterior
        else if($semanaAnyo == 53)
        {
            if( ( Fechas::getDiaSemana($anyo, $mes, 31) - 1 ) <= 2)
            {
                $semanaAnyo = 1;
            }
        }
        return $semanaAnyo;
    }
    
    /* Comprueba si un año es bisiesto */
    public static function esBisiesto($anyo)
    {            
        $a = (0 == $anyo%4); /*Año divisible por 4*/
        $b = (0 == $anyo%100); /*Año divisible por 100*/
        $c = (0 == $anyo%400);/*Año divisible por 400*/
        
        /*Un año es bisiesto si es divisible por 4 y por 400 
          o si es divisible por 4 pero no por 100 */
        $loEs = $a && ($c || !$b);
        
        return $loEs;
    }
    
    /* Devuelve el día en que acaba un mes */
    public static function getFinMes($anyo,$mes)
    {              
        /*Número de días de cada mes*/
        $diasMeses=array(31,28,31,30,31,30,31,31,30,31,30,31);
        $diasMesesBis=array(31,29,31,30,31,30,31,31,30,31,30,31);
        
        /*Según el año sea bisiesto o no, se usan unos valores u otros*/
        if( 1 == Fechas::esBisiesto($anyo) )
        {                
            $finMes = $diasMesesBis[$mes-1];
        }
        else
        {
            $finMes = $diasMeses[$mes-1];
        }

        return $finMes;
    }

}