6

he estado trabajando en una app web tipo encuesta por medio de estapas - preguntas - respuestas, ahora mismo ya tengo la parte de guardar las respuestas de cada pregunta y las preguntas en su respectiva etapa, pero en el momento de actualizar o eliminar una etapa, queda actualizada con la ultima informacion, pero por ejemplo si elimine un pregunta, no aparece visualmente, pero en la base de datos queda la pregunta con sus respuestas ocupando espacio

Describo en img cual es el problema

Imagen 1

Tengo 2 preguntas en la etapa, como se puede ver, si elimino una, tendria que quedarme como muestro en la Imagen de abajo, pero no es asi, siempre me muestra las misma, si agrego obviamente si me lo agrega, no se como eliminar esa relacion en la base de datos para que se actualize

imagen2

EstapasEdit.blade.php

@extends('layouts.layout')

@section('content')

    <form action="/dashboard/formulario/stages/{{ $stage->id }}" method="post">
        @method('put')
        <h2>ETAPA</h2>
        <label>Titulo</label>
        <input required type="text" name="title" value="{{ $stage->title }}">
        <label>Descripcion</label>
        <input required type="text" name="description" value="{{ $stage->description }}">

        <h2>PREGUNTAS</h2>
        <table>
            <tbody>
            @foreach($questions as $question)
                <tr>
                    <td><input required name="questions[{{ $question->id }}]" type="text" value="{{ $question->title }}" ><td/>
                    <td>
                        <select name="types[{{ $question->id }}]" id="">
                            @foreach($types as $type)
                                @if($type->name == $question->type->name)
                                    <option selected value="{{ $type->id }}">{{ @strtoupper($type->name) }}</option>
                                @else
                                    <option value="{{ $type->id }}">{{ @strtoupper($type->name) }}</option>
                                @endif
                            @endforeach
                        </select>
                    </td>
                    <td>
                        <fieldset>
                            <legend>Respuestas</legend>
                            @foreach($question->answers as $answer)

                                    @if($question->type->name == App\Type::OPCIONES['U'])
                                        <input required name="answers[{{ $question->id }}][{{ $answer->id }}]" type="text" value="{{ $answer->title }}">

                                    @elseif($question->type->name == App\Type::OPCIONES['C'])
                                        <input required name="answers[{{ $question->id }}][{{ $answer->id }}]" type="text" value="{{ $answer->title }}">

                                    @elseif($question->type->name == App\Type::OPCIONES['M'])
                                        <input required name="answers[{{ $question->id }}][{{ $answer->id }}]" type="text" value="{{ $answer->title }}">

                                    @endif

                            @endforeach
                        </fieldset>
                                <a href="">Agregar</a>

                    </td>
                </tr>
            @endforeach
            </tbody>
        </table>

        <input class="btn btn-primary" type="submit" value="Guardar">
    </form>

@endsection

Modelo Etapa

class Stage extends Model
{

    protected $fillable = [
        'id',
        'title',
        'description'
    ];

    public function questions(){
        return $this->hasMany('App\Question');
    }

}

Modelo Pregunta

class Question extends Model
{
    protected $table = 'questions';
    protected $fillable = [
        'id',
        'title',
        'stage_id',
        'type_id',
    ];

    public function stage() {
        return $this->belongsTo('App\Stage');
    }

    public function type(){
        return $this->belongsTo('App\Type');
    }

    public function answers(){
        return $this->hasMany('App\Answer');
    }

}

Modelo Respuesta

class Answer extends Model
{

    public function __construct(array $attributes = ['title'])
    {
        parent::__construct($attributes);
    }

    protected $table = 'answers';
    protected $fillable = [
        'title',
        'question_id'
    ];

    public function question() {
        return $this->belongsTo('App\Question');
    }

    public static function create(array $array) {
        $answers = [];


        return $answers;
    }

}

Parte del controlador que actualiza

public function update(Request $request, Stage $stage)
{

    $rules = [
        'title' => 'max:150|string|unique:stages',
        'description' => 'max:250|string'
    ];

    $stage = Stage::findOrFail($stage)->first();

    if ($request->get('title') != $stage->title ||
        $request->get('description') != $stage->description) {

        $this->validate($request, $rules);
        $stage->title = $request->get('title');
        $stage->description = $request->get('description');

        $stage->save();
    }

    /*--------- SE RECORRE TODAS LAS PREGUNTAS  ---------*/
    foreach ($request->get('questions') as $id_q => $question) {
        $question_test = new Question([
            'id' => $id_q,
            'title' => $question
        ]);
        $type = Type::findOrFail($request->get('types')[$id_q]);

        /*--------- SE ACTUALIZA LOS DATOS DE LA PREGUNTA ---------*/
        $question_test->stage()->associate($stage);
        $question_test->type()->associate($type);

        if ($question_test->title == '') {
            /*TODO: AGREGAR ENVIO DE ERROR*/
        }

        $question_test = Question::updateOrCreate([
            'id' => $question_test->id
        ],[
            'title' => $question_test->title,
            'stage_id' => $question_test->stage_id,
            'type_id' => $question_test->type_id
        ]);

        /*--------- SE ACTUALIZA LOS RESPUESTAS DE LA PREGUNTA ---------*/
        foreach ($request->get('answers') as $key => $answers) {
            if ($id_q == $key) {
                foreach ($answers as $id_a => $answer) {

                    if ($answer == '') {
                        /*TODO: AGREGAR ENVIO DE ERROR*/
                    }

                    $question_test->answers()->updateOrCreate([
                        'id' => $id_a
                    ],[
                        'title' => $answer
                    ]);

                }
            }
        }

    }

    return redirect('/dashboard/formulario/stages');

}

Migraciones

Etapas

class CreateStagesTable extends Migration
{
    /**
     * Run the migrations.
     *
     * @return void
     */
    public function up()
    {
        Schema::create('stages', function (Blueprint $table) {
            $table->increments('id');
            $table->string('title');
            $table->text('description');
            $table->timestamps();
        });
    }
    /**
     * Reverse the migrations.
     *
     * @return void
     */
    public function down()
    {
        Schema::dropIfExists('stages');
    }
}

Preguntas

class CreateQuestionsTable extends Migration
{
    /**
     * Run the migrations.
     *
     * @return void
     */
    public function up()
    {
        Schema::create('questions', function (Blueprint $table) {
            $table->increments('id');
            $table->string('title');
            $table->unsignedInteger('stage_id');
            $table->unsignedInteger('type_id');
            $table->timestamps();
            $table->foreign('stage_id')->references('id')->on('stages');
            $table->foreign('type_id')->references('id')->on('types');
        });
    }
    /**
     * Reverse the migrations.
     *
     * @return void
     */
    public function down()
    {
        Schema::table('questions', function (Blueprint $table) {
            $table->dropForeign('questions_type_id_foreign');
            $table->dropForeign('questions_stage_id_foreign');
        });
        Schema::dropIfExists('questions');
    }
}

Respuestas

class CreateAnswersTable extends Migration
{
    /**
     * Run the migrations.
     *
     * @return void
     */
    public function up()
    {
        Schema::create('answers', function (Blueprint $table) {
            $table->increments('id');
            $table->string('title');
            $table->unsignedInteger('question_id');
            $table->timestamps();
            $table->foreign('question_id')->references('id')->on('questions');
        });
    }
    /**
     * Reverse the migrations.
     *
     * @return void
     */
    public function down()
    {
        Schema::table('answers', function (Blueprint $table) {
            $table->dropForeign('answers_question_id_foreign');
        });
        Schema::dropIfExists('answers');
    }
}
7
  • Tendría que ver los FOREIGN KEY de las tablas relacionadas con las preguntas, a primera vista tal vez no esta ON DELETE CASCADE para que se eliminen las relaciones sin se elimina a la tabla con el FOREIGN KEY. mysqltutorial.org/mysql-on-delete-cascade Commented el 10 jun. 2019 a las 13:54
  • es una buena solucion, no habia pensado en el on delete cascade, pero el problema que tengo ahora es que no se como saber cual es la que voy a eliminar, porque ya no mandaria datos de ese modelo, entonces no sabria cual es para hacerle el delete Commented el 10 jun. 2019 a las 14:09
  • En principio deberías eliminar todas las relaciones si eliminas la respuesta en si, porque sus relaciones no serian útil, si tienes una tabla para almacenar las relaciones de preguntas -> respuestas, su función termina a la hora de eliminar la pregunta. Commented el 10 jun. 2019 a las 14:32
  • No es la mas refinada solución, pero eliminar todo y luego grabar lo que ya tienes en el formulario podría funcionar (al menos hasta que si encuentres una solución más digna). Mal que mal, ya tienes la lógica creada y funcional para nuevas preguntas... Commented el 12 jun. 2019 a las 21:29
  • 1
    A ver si estoy entendiendo bien: El botón eliminar elimina una pregunta? Pero lo hace mediante JavaScript y solo la remueve visualmente, es decir, no envía ninguna consulta al backend, es así? Tu problema es que el eliminar la pregunta desde el formulario NO la elimina de la base de datos? Confirmá eso y publicá los archivos de migraciones o la estructura de la DB por lo que te preguntaron de las claves foráneas.
    – azeós
    Commented el 13 jun. 2019 a las 5:09

1 respuesta 1

6
+200

El «problema» radica en que Laravel no tiene una forma (sencilla) de sincronizar relaciones uno-a-muchos, como sí lo incluye para relaciones muchos-a-muchos, y funciona de maravilla.

Aquí puedes acudir a varias soluciones:

  • Hacer tu propia implementación en el o los modelos para sincronizar los elementos que deben estar o no en la relación (puede ser un poco extenso).
  • Eliminar todas las preguntas cada vez que envías los datos de «actualización» y crear las preguntas de nuevo (NO recomendado por las consecuencias que puede acarrear el borrado de las llaves foráneas).
  • Hacer lo contrario a la solución anterior (primero actualizar y luego borrar las que no necesitas).

Por simplicidad, voy a exponer la última solución que planteo, es algo más bien rudimentario, pero te permite entender el concepto en caso que desees una implementación más profesional desde el modelo o agregando tu servicio/paquete.

Voy a mostrar únicamente las partes del método del controlador que necesito.

public function update(Request $request, Stage $stage)
{
    // ...

    $questionsToKeep = [];

    foreach ($request->get('questions') as $id_q => $question) {


        // NO ESTOY SEGURO de entender por qué haces esto...
        $question_test = new Question([
            'id' => $id_q,
            'title' => $question
        ]);

        // ...

        // La pregunta se actualiza si existe, o se crea si no
        $question_test = Question::updateOrCreate([
            'id' => $question_test->id
        ],[
            'title' => $question_test->title,
            'stage_id' => $question_test->stage_id,
            'type_id' => $question_test->type_id
        ]);


        // almacenanos el id de la pregunta que recién creamos/actualizamos
        $questionsToKeep[] = $question_test->id;

        // ...

    }

    // Una vez hayamos procesado las preguntas y respuestas que enviamos,
    // obtenemos todas las preguntas almacenadas, comparamos
    // y eliminamos aquellas que no estén en el array de "mantener"

    $allQuestions = $stage->load('questions')->questions;

    $allQuestions->each(function ($item, $key) use ($questionsToKeep) {

        // iteramos la colección de preguntas para esta etapa,
        // si el id de la pregunta no fue almacenado antes,
        // no lo necesitamos, entonces borramos esa pregunta.
        if (! in_array($item->id, $questionsToKeep)) {
            $item->delete();
        }

    });
2
  • Muchas gracias, al parecer si tenia pensado implementar un metodo, porque viendo mucho no encontraba nada acerca de ello en la documentacion. pero no habia pensado claro en eliminar las que ya no llamaba. Buenas respuesta Commented el 13 jun. 2019 a las 15:24
  • 1
    Teniendo en cuenta que tenés un array ($questionsToKeep) que contiene los IDs que NO querés borrar, en vez de traer todas las preguntas, no podrías traer directamente las que querés borrar? Algo así se me ocurre: $stage->questions()->whereNotIn('id', $questionsToKeep)->get().
    – azeós
    Commented el 13 jun. 2019 a las 17:53

¿No es la respuesta que buscas? Examina otras preguntas con la etiqueta o formula tu propia pregunta.