basement-lms icon indicating copy to clipboard operation
basement-lms copied to clipboard

Suporte á arquivos SCORM.

Open arthurtavaresdev opened this issue 3 years ago • 4 comments

Se isto se tornará de fato um LMS, é importante o mesmo ter suporte á norma SCORM, utilizada na maioria dos LMS's do Mercado.

A norma SCORM define comunicações entre o conteúdo do lado do cliente e um host chamado de ambiente de execução. O SCORM permite que você empacote seu conteúdo e o compartilhe com outros sistemas de forma mais eficiente

Leia mais sobre em: https://scorm.com/scorm-explained/technical-scorm/scorm-12-overview-for-developers/ https://pt.wikipedia.org/wiki/SCORM https://www.easy-lms.com/pt/centro-de-conhecimento/centro-conhecimento/o-que-e-scorm/item10195 https://scorm.com/scorm-explained/technical-scorm/scorm-2004-overview-for-developers/

Outras implementações: https://github.com/moodle/moodle/blob/master/mod/scorm/ https://github.com/jcputney/scorm-again https://github.com/dhodges47/SCORM-LearningManagementSystem https://github.com/mesuyog/scorm

arthurtavaresdev avatar Jul 01 '21 01:07 arthurtavaresdev

Legal! Vou dar uma lida nos conteúdos, sabe se tem alguma biblioteca pronta para resolver este problema?

NicolasPereira avatar Jul 01 '21 11:07 NicolasPereira

Infelizmente desconheço. Recomendei, pois conheço profissionais da Área de Educação, que trabalham com esse formato incorporando os mesmos a seus respectivos LMS, onde trabalham.

arthurtavaresdev avatar Jul 01 '21 22:07 arthurtavaresdev

Alguém na live deixou esse tutorial

https://github.com/EAD-Facil/SCORM-tutorial

NicolasPereira avatar Jul 19 '21 20:07 NicolasPereira

Como implementar SCORM¹

¹ (de forma simples)

Também deixo aqui a implementação de um leitor de SCORM do padrão 1.2 que funciona com qualquer arquivo SCORM criado com a ferramenta iSpring de minha autoria.

A primeira classe que criei foi a Scorm que é inicializada no construtor com o Path de um arquivo Scorm e faz a leitura do mesmo (dentro do arquivo .scorm que é um .zip na verdade).

Scorm.php

<?php
namespace Helpers;

use Exception;
use Illuminate\Support\Facades\Log;
use ZipArchive;
use SimpleXMLElement;

class Scorm {
    // private
    private $zip_path;
    private $content_path;
    private $manifest;
    private $index;
    private $name;
    private $location;

    // consts
    private const ROOT_FOLDER = 'public/storage/';
    private const ZIP_FOLDER = self::ROOT_FOLDER . 'z/';
    private const CONTENT_FOLDER = self::ROOT_FOLDER . 'c/';

    public function __construct(String $fileName) {
        $this->path = ( is_readable(self::ZIP_FOLDER . $fileName)) ? $fileName : '';
        $this->read_manifest();
        $this->name = $fileName;
        $this->location = public_path() . DIRECTORY_SEPARATOR . 'storage' . DIRECTORY_SEPARATOR;
        $this->zip_path = $this->location . $fileName;
        $this->folderName = explode('.', explode(DIRECTORY_SEPARATOR, $this->name)[2])[0];
        $this->content_path =  $this->location . 'scorm' . DIRECTORY_SEPARATOR .'c' . DIRECTORY_SEPARATOR . $this->folderName;

    }

    public function unzip() {
        try {
            if (is_readable($this->zip_path)) {
                $unzip = new ZipArchive();
                $unzip->open($this->zip_path);
                $unzip->extractTo($this->content_path);
                $unzip->close();
                return $this;
            } else {
                throw new Exception("File not " . $this->zip_path ."found", 1);
            }
        } catch (\Throwable $th) {
            Log::error($th->getMessage());
            return false;
        }
    }

    public static function getFolderPath() {
        return self::ZIP_FOLDER; 
    }

    public function getIndexPath() {
        $path = 'scorm' . DIRECTORY_SEPARATOR .'c' . DIRECTORY_SEPARATOR . $this->folderName . '/res/index.html';
        $clean_path = strtr($path, '/\\', '/');
        $clean_path = preg_replace('/\\\\+/','\\', $clean_path);

        return $clean_path;
    }

    public function getZipPath() {
        return $this->zip_path;
    }

    public function read_manifest()
    {
        if (is_readable($this->path)) {
            $zip_scorm_file = new ZipArchive();
            $contents = '';
            $file = null;

            if ($zip_scorm_file->open($this->path)) {
                $file = $zip_scorm_file->getStream('imsmanifest.xml');
            } else {
                $file = "";
            }

            if ($file !== "") {
                while (!feof($file)) {
                    $contents .= fread($file, 2);
                }
            }
            
            $manifest_contents_as_object = new SimpleXMLElement($contents);
            $this->manifest = $manifest_contents_as_object;

            $zip_scorm_file->close();
            
            return $this;
        }

    }

    public function getManifest() {
        return $this->manifest;
    }

    public function getScormVersion(): string {
        return $this->manifest->metadata->schemaversion;
    }

    public function getIndex(): string {
        return $this->content_path . 'res/index.html';
    }

    public function readIndex() {
        $zip_scorm_file = new ZipArchive();
        $html = '';

        $zip_scorm_file->open($this->path);
        
        // $html = $zip_scorm_file->statIndex( 7 ); 
        $serialization = $zip_scorm_file->getStream( $this->getIndex() ); 
        $html = stream_get_contents($serialization);

        $zip_scorm_file->close();
        return $html;
    }
}

Quando faço a função de salvar o conteúdo lido usando a classe para passar as informações relevantes para o BD em um Service do Laravel. ScormService.php (trecho)

    public function store($data)
   {
       try {
           $modulo = Modulos::find($data['modulo_id']);
           // ConteudosModulos = Conteudos
           $store = new ConteudosModulos();
           
           // pega modulo && trilha
           $store->modulo_id = $modulo->id; // id unsigned auto increment
           $store->trilha_id = $modulo->trilha->id; // id unsigned
           // guarda tipo, titulo, capa, descricao
           $store->tipo_id = 1;
           $store->titulo =  $data->titulo;
           $store->capa =  $data->capa;
           $store->descricao =  $data->descricao;

           // $store->disponivel
           $dia= (!empty($data['disponivel_data'])?null:date('Y-m-d', strtotime(str_replace('/', '-', $data['disponivel_data']))));
           $hora =  (!empty($data['disponivel_hora'])?null: $data['disponivel_hora']);
           if(empty($dia) || empty($hora)){
               $disponivel = null;
           }else{
               $disponivel = $dia.' '. $hora;
           }
           $store->disponivel = (empty($disponivel)? null : $disponivel);
           // end $store->disponivel

           // salva o conteúdo
           if ($data->has('uploaded_file')) {
               $scormHandler = new Scorm($data->uploaded_file);
               $store->arquivo = $scormHandler->unzip()->getIndexPath();
               $store->is_scorm = true;
           } else {
               throw new Exception("Erro na leitura do arquivo", 1);               
           }

           $store->ordem = $modulo->conteudos->count()+1;
           $store->save();
           return $store;
       } catch (\Exception $exception) {
           logger()->error($exception);
           return false;
       }
   }

E na hora de salvar no Controller uso regex para fazer o clean dos dados e chamo o service para cuidar disso. ScormContentController.php método store* (trecho)

if ($request->hasFile('scorm')) {
            $file = $request->file('scorm');
            
            $path = Storage::disk('public')->putFileAs('scorm' . DIRECTORY_SEPARATOR  . 'z', new File($file->getRealPath()), (DIRECTORY_SEPARATOR . time() . $file->getClientOriginalName() ) );
            
            $path = strtr($path, '/\\', DIRECTORY_SEPARATOR);
            $path = preg_replace('/\\\\+/','\\', $path);
            $path = preg_replace('#/+#','/',$path);
            
            $request->merge(['uploaded_file' => $path]);

            if ($this->service->store($request)) {
                return redirect()->back()->with(['type' => 'success', 'message' => 'Item cadastrado com sucesso']);
            }
        } else {
            return redirect()->back()->with(['type' => 'error', 'message' => 'Erro ao cadastrar o item']);
        }

No front-end eu carrego um iframe que chama o path do arquivo inicial html do Scorm que está salvo no servidor e que foi salvo no BD apontando para seu local na pasta storage do Laravel.

@if($conteudo->is_scorm)
    <iframe name="myframe" src="{{ URL::to('/')}}/storage/{{$conteudo->arquivo}}" height="450" width="100%" style="border: none !important;">Your browser does not support frames.</iframe>
@endif

E isso carrega o SCORM 1.2 do iSpring dentro do iframe e tudo funciona como devia, utilizando os cookies do Navegador para salvar as informações de progresso do curso, notas e demais.

Depois disso é facil fazer a implementação de ler essas infos dos cookies e passar por API ou Ajax para o backend para fazer a integração do SCORM com o sistema.

É uma idéia de como pode funcionar, infelizmente essa técnica apenas funciona com o iSpring, mas a vantagem é que é fácil de implementar para ler arquivos porém com o Rusticii Driver que explico no link do @NicolasPereira é possivel exportar de forma facilitada, a técnica que usei com o iSpring pode facilmente ser adaptada para funcionar com os arquivos gerados pelo Rustici Driver e assim tendo uma integração SCORM minimamente funcional, simples e gratuita.

Espero que ajude!

Mais links:

Camilotk avatar Aug 08 '21 17:08 Camilotk