custom widget not saved to sidebar

Alright, so I found it after some sleepless hours:

  1. The call to $this->get_field_id($key) in the Widget constructor was a mistake. It has to happen during the function form() (probably due to some usage of global variables* )
  2. Most of the pain came from the fact, that it did not save the widget in the sidebar. The reason for this was, that it was all getting messed up during development in the database. WordPress seems to expect a certain order between the id_base and the actual created instances in the database. If you want a clear picture, try to change the id_base to something fresh.

The working code in the end:

class DW_Center_Address_Widget extends WP_Widget {

    // I changed the id_base a couple of times
    const ID = 'dwbn-center-address-3';

    var $formDefinition;
    var $widgetArguments = [];

    function __construct() {
        //Constructor
        $widget_ops = array('classname' => self::ID, 'description' => Connector::get_translation( 'Displays the address of a Center stored in the SSO.' ) );
        parent::__construct( self::ID, Connector::get_translation('DW Center Address'), $widget_ops );

        $this->formDefinition = [
            // a title field makes the wordpress widget definetly more usefull, it will display this value in gray
            [
                'type' => 'text',
                'key' => Shortcodes::ATTR_TITLE,
                'label' => __('Title'), // just take the standard translation
                'value' => '' // the default value
            ],
            [
                'type' => 'info',
                'label' => Connector::get_translation("Change Center data at the:") . '<br/>' . '<a target="_blank" class="button" style="margin-top: 10px;" href="https://wordpress.stackexchange.com/questions/338970/the.sso">' . Connector::get_translation('SSO') . '</a>'
            ],
            [
                'type' => 'text',
                'key' => Shortcodes::ATTR_SLUG,
                'label' => Connector::get_translation('Center Slug or Id'),
                'value' => '' // the default value
            ]
        ];

        foreach ($this->formDefinition as $def) {
            if (array_key_exists('key', $def)) {
                $this->widgetArguments[$def['key']] = array_key_exists('value', $def) ? $def['value'] : '';
            }
        }
    }

    /**
     * @param array $args - holds context information
     * @param array $instance - the actual parameters
     */
    function widget($args, $instance) {
        echo Shortcodes::dw_center($instance);
    }

    function update($new_instance, $old_instance) {
        $instance = [];

        foreach ($this->widgetArguments as $key => $val) {
            $instance[ $key ] = trim(strip_tags($new_instance[ $key ]));
        }

        return $instance;
    }

    function form( $instance ) {
        $instance = wp_parse_args( (array) $instance, $this->widgetArguments );
        $form = [];

        for ($i = 0; $i < count($this->formDefinition); $i++) {

            $form[] = $this->formDefinition[$i];

            if (array_key_exists('key', $form[$i])) {
                $key = $form[$i]['key'];

                if (array_key_exists($key, $instance)) {
                    $form[$i]['value'] = $instance[$key];
                }

                // it is important, that this functions are called during runtime, otherwise the widget ids will not be correct
                $form[$i]['id'] = $this->get_field_id($key);
                $form[$i]['name'] = $this->get_field_name($key);
            }
        }

        echo Template::render('admin/forms/widget_form.html.twig', [
            'form' => $form
        ]);

    }
}

* this is jet one more example why not to rely on a global namespace – wordpress will not change, but if you ever build a new system that millions of people will use, please try to avoid it ;).